From 2e7711ee2e6a3c2041395586616a452ca13a0483 Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Mon, 9 Nov 2026 15:59:13 +0700 Subject: [PATCH] Move layout metrics into theme and apply audit cleanups --- crates/core/src/alert.rs | 16 +- crates/core/src/avatar.rs | 29 +- crates/core/src/button.rs | 8 +- crates/core/src/combobox.rs | 38 +-- crates/core/src/command_palette.rs | 8 +- crates/core/src/dialog.rs | 41 +-- crates/core/src/dropdown_menu.rs | 4 +- crates/core/src/input.rs | 219 ++++++------ crates/core/src/menu.rs | 14 +- crates/core/src/scroll_area.rs | 321 ++---------------- crates/core/src/scrollbar.rs | 135 +++++++- crates/core/src/separator.rs | 8 +- crates/core/src/slider.rs | 6 - crates/core/src/tooltip.rs | 8 +- crates/data/src/sidebar.rs | 26 +- crates/data/src/tabs.rs | 3 +- crates/data/src/tree.rs | 48 ++- crates/editor/src/code_view.rs | 25 +- crates/editor/src/diff_view.rs | 4 +- crates/editor/src/markdown.rs | 54 +-- crates/editor/tests/markdown.rs | 8 - crates/layout/Cargo.toml | 1 - crates/layout/src/dock.rs | 36 +- crates/layout/src/resize_handle.rs | 12 +- crates/layout/src/shell.rs | 60 ++-- crates/layout/src/window_controls.rs | 19 +- crates/layout/src/window_frame.rs | 16 +- crates/layout/tests/dock.rs | 16 +- crates/layout/tests/window_controls.rs | 20 +- crates/theme/src/lib.rs | 34 +- docs/component-audit.md | 53 ++- examples/gallery/src/main.rs | 444 ++++++++++--------------- 32 files changed, 762 insertions(+), 972 deletions(-) diff --git a/crates/core/src/alert.rs b/crates/core/src/alert.rs index f74eb28..8af6b5a 100644 --- a/crates/core/src/alert.rs +++ b/crates/core/src/alert.rs @@ -260,11 +260,19 @@ impl Alerts { } } - /// Dismisses every alert. + /// Dismisses every alert, running each entry's `on_dismiss` handler. pub fn clear(&mut self, cx: &mut Context) { - if !self.entries.is_empty() { - self.entries.clear(); - cx.notify(); + if self.entries.is_empty() { + return; + } + + let entries = std::mem::take(&mut self.entries); + cx.notify(); + + for entry in entries { + if let Some(on_dismiss) = entry.on_dismiss { + on_dismiss(cx); + } } } diff --git a/crates/core/src/avatar.rs b/crates/core/src/avatar.rs index af87432..a5cfb9a 100644 --- a/crates/core/src/avatar.rs +++ b/crates/core/src/avatar.rs @@ -4,7 +4,10 @@ use gpui::{ App, ElementId, InteractiveElement as _, IntoElement, ParentElement as _, RenderOnce, Role, SharedString, StatefulInteractiveElement as _, StyleRefinement, Styled as _, Window, div, }; -use gpui_native_theme::{ActiveTheme as _, Oklch, Size, ThemeVariant}; +use gpui_native_theme::{ + ActiveTheme as _, Oklch, PIXEL_AVATAR_DARK_FACTOR, PIXEL_AVATAR_DARK_FLOOR, + PIXEL_AVATAR_LIGHT_CAP, PIXEL_AVATAR_LIGHT_LIFT, PIXEL_AVATAR_TINT_ALPHA, Size, ThemeVariant, +}; use crate::common::{Sizable, StyledExt, h_flex}; @@ -108,12 +111,10 @@ impl RenderOnce for Avatar { .refine_style(&self.style); match self.picture { - Some(url) => base.child(gpui::img(url).size_full().rounded(radius)), + Some(url) => base.child(gpui::img(url).size_full()), None => base.child(PixelAvatar { seed: fnv1a(self.id.as_bytes()), side, - radius, - style: self.style, }), } .into_any_element() @@ -125,14 +126,6 @@ impl RenderOnce for Avatar { struct PixelAvatar { seed: u64, side: gpui::Pixels, - radius: gpui::Pixels, - style: StyleRefinement, -} - -impl gpui::Styled for PixelAvatar { - fn style(&mut self) -> &mut StyleRefinement { - &mut self.style - } } impl RenderOnce for PixelAvatar { @@ -147,13 +140,13 @@ impl RenderOnce for PixelAvatar { let shade = if theme.variant == ThemeVariant::Dark { Oklch { - l: (accent.l * 1.6).min(0.95), + l: (accent.l * PIXEL_AVATAR_LIGHT_LIFT).min(PIXEL_AVATAR_LIGHT_CAP), ..Oklch::from_hsla(main) } .to_hsla() } else { Oklch { - l: (accent.l * 0.45).max(0.18), + l: (accent.l * PIXEL_AVATAR_DARK_FACTOR).max(PIXEL_AVATAR_DARK_FLOOR), ..Oklch::from_hsla(main) } .to_hsla() @@ -182,11 +175,11 @@ impl RenderOnce for PixelAvatar { .grid_rows(GRID_SIZE as u16) .size(self.side) .flex_none() - .rounded(self.radius) - .overflow_hidden() - .bg(gpui::Hsla { a: 0.16, ..main }) + .bg(gpui::Hsla { + a: PIXEL_AVATAR_TINT_ALPHA, + ..main + }) .children(cells) - .refine_style(&self.style) } } diff --git a/crates/core/src/button.rs b/crates/core/src/button.rs index 524ceef..315e2a2 100644 --- a/crates/core/src/button.rs +++ b/crates/core/src/button.rs @@ -4,7 +4,7 @@ use gpui::prelude::FluentBuilder as _; use gpui::{ AnyView, App, ClickEvent, ElementId, InteractiveElement as _, IntoElement, ParentElement as _, RenderOnce, Role, SharedString, StatefulInteractiveElement as _, Styled as _, Window, canvas, - hsla, relative, + relative, transparent_black, }; use gpui_native_icons::{IconRole, icon}; use gpui_native_theme::{ActiveTheme as _, BORDER_WIDTH, Size}; @@ -199,9 +199,9 @@ impl RenderOnce for Button { match background { Some(fill) => (fill, hover, pressed), None => ( - hsla(0.0, 0.0, 0.0, 0.0), - hsla(0.0, 0.0, 0.0, 0.0), - hsla(0.0, 0.0, 0.0, 0.0), + transparent_black(), + transparent_black(), + transparent_black(), ), } }; diff --git a/crates/core/src/combobox.rs b/crates/core/src/combobox.rs index 3367308..8329c10 100644 --- a/crates/core/src/combobox.rs +++ b/crates/core/src/combobox.rs @@ -10,13 +10,13 @@ use std::rc::Rc; use gpui::prelude::FluentBuilder as _; use gpui::{ - App, Bounds, Context, ElementId, Entity, EventEmitter, InteractiveElement as _, IntoElement, - KeyBinding, MouseButton, MouseDownEvent, ParentElement as _, Pixels, Render, Role, - ScrollHandle, SharedString, StatefulInteractiveElement as _, StyleRefinement, Styled, - Subscription, Task, Window, actions, anchored, canvas, deferred, div, point, px, + App, Bounds, Context, ElementId, Entity, InteractiveElement as _, IntoElement, KeyBinding, + MouseButton, MouseDownEvent, ParentElement as _, Pixels, Render, Role, ScrollHandle, + SharedString, StatefulInteractiveElement as _, StyleRefinement, Styled, Subscription, Task, + Window, actions, anchored, canvas, deferred, div, point, px, }; use gpui_native_icons::{IconRole, icon}; -use gpui_native_theme::{ActiveTheme as _, BORDER_WIDTH, Size}; +use gpui_native_theme::{ActiveTheme as _, BORDER_WIDTH, POPUP_MAX_HEIGHT, Size}; use crate::common::{Sizable, StyledExt as _, h_flex, v_flex}; use crate::input::{Input, InputState, InputStateEvent}; @@ -177,9 +177,6 @@ where } } -/// Emitted when the combobox's value changes. -pub struct ComboboxChanged; - /// The stateful combobox. pub struct ComboboxState where @@ -209,8 +206,6 @@ where _subscription: Subscription, } -impl EventEmitter for ComboboxState where T: Clone + PartialEq + 'static {} - impl Sizable for ComboboxState where T: Clone + PartialEq + 'static, @@ -316,8 +311,8 @@ where .map(|(_, label)| label.clone()) } - /// Recomputes `visible` for `query` and hands the survivors' labels back. - fn refilter(&mut self, query: &str) -> Vec { + /// Recomputes `visible` for `query` and clamps the highlight onto it. + fn refilter(&mut self, query: &str) { let filter = self.filter.clone(); self.visible = self .options @@ -326,10 +321,7 @@ where .filter(|(_, (value, label))| filter(query, value, label)) .map(|(index, _)| index) .collect(); - self.visible - .iter() - .map(|index| self.options[*index].1.clone()) - .collect() + self.highlight = self.highlight.filter(|row| *row < self.visible.len()); } fn query_text(&self, cx: &Context) -> String { @@ -349,7 +341,6 @@ where }); self.tasks.push(task); } - cx.emit(ComboboxChanged); cx.notify(); } @@ -375,7 +366,7 @@ where fn text_changed(&mut self, window: &mut Window, cx: &mut Context) { let query = self.query_text(cx); self.refilter(&query); - self.highlight = (!self.visible.is_empty()).then_some(0usize); + self.highlight = Some(0); self.set_value(None, window, cx); cx.notify(); } @@ -386,7 +377,7 @@ where return; } self.refilter(""); - self.highlight = (!self.visible.is_empty()).then_some(0usize); + self.highlight = Some(0); cx.notify(); } @@ -427,10 +418,7 @@ where if self.visible.is_empty() { return; } - let current = self - .highlight - .and_then(|row| self.visible.get(row).copied()) - .and_then(|option_ix| self.visible.iter().position(|&row| row == option_ix)); + let current = self.highlight.filter(|row| *row < self.visible.len()); let next = match current { Some(row) => { let row = (row as i32 + delta).rem_euclid(self.visible.len() as i32); @@ -516,7 +504,7 @@ where let query = state.query.read(cx).value().to_string(); state.refilter(&query); } - state.highlight = (!state.visible.is_empty()).then_some(0usize); + state.highlight = Some(0); } cx.notify(); }); @@ -667,7 +655,7 @@ where ScrollArea::new((id.clone(), "rows")) .handle(scroll.read(cx).clone()) .axes(crate::scroll_area::ScrollAxes::Vertical) - .max_h(px(320.)) + .max_h(POPUP_MAX_HEIGHT) .children(rows) .into_any_element() }; diff --git a/crates/core/src/command_palette.rs b/crates/core/src/command_palette.rs index be2a201..213845a 100644 --- a/crates/core/src/command_palette.rs +++ b/crates/core/src/command_palette.rs @@ -7,10 +7,10 @@ use gpui::{ AnyElement, App, Context, ElementId, Entity, InteractiveElement as _, IntoElement, KeyBinding, MouseButton, ParentElement as _, Render, Role, ScrollHandle, SharedString, StatefulInteractiveElement as _, Styled as _, Subscription, Window, actions, canvas, deferred, - div, px, relative, + div, relative, }; use gpui_native_icons::{IconRole, icon}; -use gpui_native_theme::{ActiveTheme as _, Size}; +use gpui_native_theme::{ActiveTheme as _, POPUP_MAX_HEIGHT, Size}; use crate::common::{h_flex, v_flex}; use crate::input::{Input, InputState, InputStateEvent}; @@ -333,7 +333,7 @@ impl Render for CommandPaletteState { let row_ix = row; h_flex() - .id(("command", row)) + .id((ElementId::from(command.id.clone()), "command")) .debug_selector({ let id = id.clone(); move || format!("{id}-command-{row}") @@ -401,7 +401,7 @@ impl Render for CommandPaletteState { ScrollArea::new((id.clone(), "rows")) .handle(scroll.read(cx).clone()) .axes(ScrollAxes::Vertical) - .max_h(px(320.)) + .max_h(POPUP_MAX_HEIGHT) .children(rows) .into_any_element() }; diff --git a/crates/core/src/dialog.rs b/crates/core/src/dialog.rs index 3856ebc..6aba2f4 100644 --- a/crates/core/src/dialog.rs +++ b/crates/core/src/dialog.rs @@ -1,12 +1,10 @@ -use std::cell::RefCell; +use gpui::prelude::FluentBuilder as _; use std::rc::Rc; -use gpui::prelude::FluentBuilder as _; use gpui::{ - Animation, AnimationExt as _, AnyElement, App, Bounds, ElementId, FontWeight, - InteractiveElement as _, IntoElement, KeyDownEvent, MouseButton, MouseDownEvent, - ParentElement as _, Pixels, RenderOnce, Role, SharedString, StatefulInteractiveElement as _, - Styled as _, Window, canvas, div, ease_out_quint, relative, + Animation, AnimationExt as _, AnyElement, App, ElementId, FontWeight, InteractiveElement as _, + IntoElement, KeyDownEvent, MouseButton, ParentElement as _, RenderOnce, Role, SharedString, + StatefulInteractiveElement as _, Styled as _, Window, canvas, div, ease_out_quint, relative, }; use gpui_native_theme::ActiveTheme as _; @@ -16,9 +14,6 @@ use crate::common::v_flex; pub type ChangeHandler = Rc; type ContentFn = Box AnyElement>; type ActionsFn = Box Vec>; -/// Bounds captured during the last paint, shared with the scrim listener. -type BoundsCell = Rc>>>; - /// A centred panel over a dim scrim: the platform's modal. #[derive(IntoElement)] pub struct Dialog { @@ -165,7 +160,6 @@ impl RenderOnce for Dialog { let content = self.content.map(|build| build(window, cx)); let actions = self.actions.map(|build| build(window, cx)); - let panel_bounds: BoundsCell = Rc::new(RefCell::new(None)); let panel = v_flex() .id("dialog-panel") @@ -211,19 +205,13 @@ impl RenderOnce for Dialog { panel.child(div().flex().justify_end().gap_2().children(actions)) }) .child( - canvas( - { - let cell = panel_bounds.clone(); - move |bounds, _window, _cx| *cell.borrow_mut() = Some(bounds) - }, - { - move |_, _, window, cx| { - if !focus.contains_focused(window, cx) { - window.focus(&focus, cx); - } + canvas(|_, _, _| {}, { + move |_, _, window, cx| { + if !focus.contains_focused(window, cx) { + window.focus(&focus, cx); } - }, - ) + } + }) .absolute() .inset_0(), ); @@ -235,15 +223,8 @@ impl RenderOnce for Dialog { .occlude() .when(!alert, |scrim| { scrim.on_mouse_down(MouseButton::Left, { - let panel_bounds = panel_bounds.clone(); let on_change = on_change.clone(); - move |event: &MouseDownEvent, _window, cx| { - let on_panel = panel_bounds - .borrow() - .is_some_and(|bounds| bounds.contains(&event.position)); - if on_panel { - return; - } + move |_, _window, cx| { if let Some(on_change) = &on_change { on_change(false, _window, cx); } diff --git a/crates/core/src/dropdown_menu.rs b/crates/core/src/dropdown_menu.rs index 8873e83..1f6a976 100644 --- a/crates/core/src/dropdown_menu.rs +++ b/crates/core/src/dropdown_menu.rs @@ -7,7 +7,7 @@ use gpui::prelude::FluentBuilder as _; use gpui::{ App, ClickEvent, DispatchPhase, ElementId, InteractiveElement as _, IntoElement, KeyDownEvent, MouseButton, MouseDownEvent, ParentElement as _, RenderOnce, Role, SharedString, - StatefulInteractiveElement as _, Styled as _, Window, canvas, div, hsla, relative, + StatefulInteractiveElement as _, Styled as _, Window, canvas, div, relative, }; use gpui_native_icons::{IconRole, icon}; use gpui_native_theme::{ActiveTheme as _, BORDER_WIDTH, Size}; @@ -193,7 +193,7 @@ impl RenderOnce for DropdownMenu { let border = if show_border { colors.border } else { - background.unwrap_or_else(|| hsla(0.0, 0.0, 0.0, 0.0)) + background.unwrap_or_else(gpui::transparent_black) }; let split = self.on_click.is_some(); diff --git a/crates/core/src/input.rs b/crates/core/src/input.rs index 0577f27..d8b32d1 100644 --- a/crates/core/src/input.rs +++ b/crates/core/src/input.rs @@ -1,4 +1,3 @@ -use std::cell::RefCell; use std::ops::Range; use std::rc::Rc; use std::time::{Duration, Instant}; @@ -16,7 +15,10 @@ use gpui::{ WrappedLine, actions, canvas, div, fill, point, px, }; use gpui_native_icons::IconRole; -use gpui_native_theme::{ActiveTheme as _, BORDER_WIDTH, ColorTokens, Size}; +use gpui_native_theme::{ + ActiveTheme as _, BORDER_WIDTH, CARET_WIDTH, ColorTokens, Size, UNDERLINE_WIDTH, + WHEEL_LINE_HEIGHT, +}; use gpui_native_utils::focus_ring; use crate::button::{Button, ButtonVariant}; @@ -29,7 +31,7 @@ const DEFAULT_GROUP_INTERVAL: Duration = Duration::from_millis(300); /// How often the focused caret blinks. const CARET_BLINK_INTERVAL: Duration = Duration::from_millis(500); /// Pixel height of one scroll "line" for wheel scrolling. -const SCROLL_LINES_HEIGHT: f32 = 20.0; +const SCROLL_LINES_HEIGHT: f32 = WHEEL_LINE_HEIGHT; /// The row count a multiline field shows until the app sets one. const DEFAULT_ROWS: usize = 4; /// Maximum undo entries kept; the oldest drops off the front. @@ -270,7 +272,7 @@ impl InputBindings { } /// Collects the table into keybindings. - pub fn into_bindings(self) -> Vec { + pub(crate) fn into_bindings(self) -> Vec { let mut bindings: Vec = [ self.escape, self.submit, @@ -491,9 +493,9 @@ pub struct InputState { focused: bool, focus_handle: gpui::FocusHandle, /// The key-down handler, bound to this entity with `cx.listener`. - key_down_handler: Option, + key_down_handler: KeyDownHandler, /// The mouse-down handler, bound to this entity with `cx.listener`. - mouse_down_handler: Option, + mouse_down_handler: MouseDownHandler, /// Whether a drag-select is in progress. dragging: bool, /// Patch history. `redo_stack` holds undone entries awaiting redo. @@ -520,9 +522,9 @@ pub struct InputState { /// Bumped on every edit. caret_epoch: usize, /// When the current caret epoch started. - caret_epoch_started: Option, + caret_epoch_started: Instant, /// The shaped line and element bounds, recorded during each paint. - layout: Rc>>, + layout: Option, } impl EventEmitter for InputState {} @@ -536,16 +538,14 @@ impl InputState { marked_range: None, focused: false, focus_handle: cx.focus_handle(), - key_down_handler: Some(Rc::new(cx.listener( - |state, event: &KeyDownEvent, window, cx| { - state.handle_key_down(event, window, cx); - }, - ))), - mouse_down_handler: Some(Rc::new(cx.listener( + key_down_handler: Rc::new(cx.listener(|state, event: &KeyDownEvent, window, cx| { + state.handle_key_down(event, window, cx); + })), + mouse_down_handler: Rc::new(cx.listener( |state, event: &MouseDownEvent, window, cx| { state.handle_mouse_down(event, window, cx); }, - ))), + )), dragging: false, undo_stack: Vec::new(), redo_stack: Vec::new(), @@ -559,8 +559,8 @@ impl InputState { scroll_offset: point(Pixels::ZERO, Pixels::ZERO), scroll_to_caret_pending: true, caret_epoch: 0, - caret_epoch_started: Some(Instant::now()), - layout: Rc::new(RefCell::new(None)), + caret_epoch_started: Instant::now(), + layout: None, } } @@ -696,12 +696,7 @@ impl InputState { } } - /// Which keystroke submits a multiline field. - pub fn submit_on(&self) -> SubmitOn { - self.submit_on - } - - /// Whether the painted line masks the buffer. + // Whether the painted line masks the buffer. fn masked(&self) -> bool { self.password && !self.revealed } @@ -711,32 +706,43 @@ impl InputState { self.redo_stack.clear(); let now = Instant::now(); - if let Some(last) = self.undo_stack.last_mut() - && now.duration_since(last.timestamp) < self.group_interval - { + if let Some(last) = self.undo_stack.last_mut() { let last_end = last.range.start + last.new_len; - // Typing that continues right after the previous edit. - if range.start == last_end { - last.new_len += new_len; - last.timestamp = now; - return; + if self.marked_range.is_some() { + // A pre-edit step overlapping the previous entry's region. + if range.start >= last.range.start { + last.new_len += new_len; + let replaced = range.len(); + last.new_len -= replaced; + last.timestamp = now; + return; + } } - // A deletion of text that sat right before the previous edit. - if range.end == last.range.start && new_len == 0 { - last.range.start = range.start; - last.old_text = old_text + &last.old_text; - last.timestamp = now; - return; - } + if now.duration_since(last.timestamp) < self.group_interval { + // Typing that continues right after the previous edit. + if range.start == last_end { + last.new_len += new_len; + last.timestamp = now; + return; + } - // An edit fully inside the previous edit's replacement. - if range.start >= last.range.start && range.end <= last_end { - last.new_len += new_len; - last.new_len -= range.len(); - last.timestamp = now; - return; + // A deletion of text that sat right before the previous edit. + if range.end == last.range.start && new_len == 0 { + last.range.start = range.start; + last.old_text = old_text + &last.old_text; + last.timestamp = now; + return; + } + + // An edit fully inside the previous edit's replacement. + if range.start >= last.range.start && range.end <= last_end { + last.new_len += new_len; + last.new_len -= range.len(); + last.timestamp = now; + return; + } } } @@ -769,7 +775,7 @@ impl InputState { self.scroll_to_caret_pending = true; self.goal_x = None; self.caret_epoch += 1; - self.caret_epoch_started = Some(Instant::now()); + self.caret_epoch_started = Instant::now(); window.notify_text_input_changed(&self.focus_handle); } @@ -781,20 +787,38 @@ impl InputState { cx: &mut Context, ) -> bool { if self.multiline { - self.scroll_offset.y = (self.scroll_offset.y + delta.y).max(Pixels::ZERO); + let next_y = (self.scroll_offset.y + delta.y).max(Pixels::ZERO); + if next_y == self.scroll_offset.y { + return self.multiline_scrollable(); + } + self.scroll_offset.y = next_y; } else { - self.scroll_offset.x = (self.scroll_offset.x - delta.x).max(Pixels::ZERO); + let next_x = (self.scroll_offset.x - delta.x).max(Pixels::ZERO); + if next_x == self.scroll_offset.x { + return false; + } + self.scroll_offset.x = next_x; } + let scrollable = self .layout - .borrow() .as_ref() .is_some_and(|layout| layout.content_height > layout.visible_height); + cx.notify(); window.notify_text_input_changed(&self.focus_handle); self.multiline && scrollable } + /// Whether a multiline field's content overflows vertically. + fn multiline_scrollable(&self) -> bool { + self.multiline + && self + .layout + .as_ref() + .is_some_and(|layout| layout.content_height > layout.visible_height) + } + /// Emits `Focused`/`Blurred` when the field's focus state changed since the last paint. fn sync_focus(&mut self, focused: bool, cx: &mut Context) { if self.focused == focused { @@ -802,11 +826,9 @@ impl InputState { } self.focused = focused; - // A fresh focus starts a fresh blink epoch, so the caret shows solid - // for one interval before blinking resumes. if focused { self.caret_epoch += 1; - self.caret_epoch_started = Some(Instant::now()); + self.caret_epoch_started = Instant::now(); } cx.emit(if focused { @@ -817,11 +839,8 @@ impl InputState { } /// Whether the caret should paint. - pub fn caret_visible(&mut self, focused: bool) -> bool { - let elapsed = self - .caret_epoch_started - .map(|started| started.elapsed()) - .unwrap_or_default(); + pub fn caret_visible(&self, focused: bool) -> bool { + let elapsed = self.caret_epoch_started.elapsed(); self.caret_blink_visible_at(focused, elapsed) } @@ -901,15 +920,21 @@ impl InputState { window: &mut Window, cx: &mut Context, ) { - self.cursor = to.clamp(0, self.text.len()); - if !extend { - self.anchor = self.cursor; + let cursor = to.clamp(0, self.text.len()); + let anchor = if extend { self.anchor } else { cursor }; + if cursor == self.cursor && anchor == self.anchor { + return; } + self.cursor = cursor; + self.anchor = anchor; self.notify(window, cx); } /// Selects `range`. fn select_range(&mut self, range: Range, window: &mut Window, cx: &mut Context) { + if range.start == self.anchor && range.end == self.cursor { + return; + } self.anchor = range.start; self.cursor = range.end; self.notify(window, cx); @@ -1085,7 +1110,7 @@ impl InputState { ); let text_width = (bounds.size.width - padding.left - padding.right).max(Pixels::ZERO); - *self.layout.borrow_mut() = Some(InputLayout { + self.layout = Some(InputLayout { lines: text.lines, rows, bounds, @@ -1107,8 +1132,7 @@ impl InputState { return; } - let layout = self.layout.borrow(); - let Some(layout) = layout.as_ref() else { + let Some(layout) = self.layout.as_ref() else { return; }; @@ -1138,8 +1162,7 @@ impl InputState { window: &mut Window, cx: &mut App, ) { - let layout = self.layout.borrow(); - let Some(layout) = layout.as_ref() else { + let Some(layout) = self.layout.as_ref() else { return; }; @@ -1155,17 +1178,16 @@ impl InputState { let mut line_top = Pixels::ZERO; for entry in &layout.lines { let line_origin = point(origin.x, origin.y + line_top); - entry - .line - .paint( - line_origin, - layout.line_height, - TextAlign::Left, - None, - window, - cx, - ) - .ok(); + if let Err(err) = entry.line.paint( + line_origin, + layout.line_height, + TextAlign::Left, + None, + window, + cx, + ) { + eprintln!("painting input text failed: {err}"); + } line_top += layout.line_height * (entry.line.wrap_boundaries().len() + 1) as f32; } }); @@ -1179,8 +1201,7 @@ impl InputState { color: gpui::Hsla, window: &mut Window, ) { - let layout = self.layout.borrow(); - let Some(layout) = layout.as_ref() else { + let Some(layout) = self.layout.as_ref() else { return; }; @@ -1192,7 +1213,7 @@ impl InputState { let caret_top = origin.y + layout.rows[row_ix].y + (layout.line_height - font_size) / 2.0; let caret = Bounds::from_corners( point(origin.x + caret_x, caret_top), - point(origin.x + caret_x + px(1.5), caret_top + font_size), + point(origin.x + caret_x + CARET_WIDTH, caret_top + font_size), ); window.paint_quad(fill(caret, color)); @@ -1242,8 +1263,7 @@ impl InputState { /// The text index at a window point, using the last painted layout. fn index_at(&self, position: gpui::Point) -> usize { - let layout = self.layout.borrow(); - let Some(layout) = layout.as_ref() else { + let Some(layout) = self.layout.as_ref() else { return 0; }; real_index(&layout.mask_map, layout.index_at(position)) @@ -1256,7 +1276,11 @@ impl InputState { window: &mut Window, cx: &mut Context, ) { - self.cursor = self.index_at(position); + let cursor = self.index_at(position); + if cursor == self.cursor { + return; + } + self.cursor = cursor; self.notify(window, cx); } @@ -1268,14 +1292,13 @@ impl InputState { color: gpui::Hsla, window: &mut Window, ) { - let layout = self.layout.borrow(); - let Some(layout) = layout.as_ref() else { + let Some(layout) = self.layout.as_ref() else { return; }; let start = display_index(&layout.mask_map, range.start); let end = display_index(&layout.mask_map, range.end); - let thickness = px(1.0); + let thickness = UNDERLINE_WIDTH; for (row_ix, clipped, beyond) in layout.row_overlaps(start..end) { let top = origin.y + layout.rows[row_ix].y + layout.line_height - thickness; @@ -1329,7 +1352,7 @@ impl InputState { if event.click_count >= 3 { // Triple-click selects the clicked row, which is all of it when single-line. - let row_range = self.layout.borrow().as_ref().and_then(|layout| { + let row_range = self.layout.as_ref().and_then(|layout| { let display = display_index(&layout.mask_map, index); layout.row_range_at(display).map(|range| { real_index(&layout.mask_map, range.start) @@ -1462,15 +1485,16 @@ impl InputState { } let target = { - let layout = self.layout.borrow(); - let Some(layout) = layout.as_ref() else { + let Some(layout) = self.layout.as_ref() else { return; }; let display = display_index(&layout.mask_map, self.cursor); + let Some((row_ix, caret_x)) = layout.row_for_index(display) else { return; }; let target = row_ix as isize + delta; + let (display, goal_x) = if target < 0 { (0, None) } else if target as usize >= layout.rows.len() { @@ -1479,6 +1503,7 @@ impl InputState { let goal_x = self.goal_x.unwrap_or(caret_x); (layout.index_at_row_x(target as usize, goal_x), Some(goal_x)) }; + Some((real_index(&layout.mask_map, display), goal_x)) }; @@ -1748,8 +1773,7 @@ impl EntityInputHandler for InputState { _window: &mut Window, _cx: &mut Context, ) -> Option> { - let layout = self.layout.borrow(); - let layout = layout.as_ref()?; + let layout = self.layout.as_ref()?; let start = display_index( &layout.mask_map, @@ -1797,8 +1821,7 @@ impl EntityInputHandler for InputState { _window: &mut Window, _cx: &mut Context, ) -> Option { - let layout = self.layout.borrow(); - let layout = layout.as_ref()?; + let layout = self.layout.as_ref()?; let index = real_index(&layout.mask_map, layout.index_at(window_point)).min(self.text.len()); Some(utf8_range_to_utf16(&self.text, index..index).start) @@ -2178,23 +2201,13 @@ impl RenderOnce for Input { .when(!self.disabled, |input| { input .on_mouse_down(MouseButton::Left, { - let mouse_down_handler = self - .state - .read(cx) - .mouse_down_handler - .clone() - .expect("input state always carries its mouse-down handler"); + let mouse_down_handler = self.state.read(cx).mouse_down_handler.clone(); move |event: &MouseDownEvent, window, cx| { mouse_down_handler(event, window, cx); } }) .on_key_down({ - let key_down_handler = self - .state - .read(cx) - .key_down_handler - .clone() - .expect("input state always carries its key handler"); + let key_down_handler = self.state.read(cx).key_down_handler.clone(); move |event: &KeyDownEvent, window, cx| { key_down_handler(event, window, cx); } diff --git a/crates/core/src/menu.rs b/crates/core/src/menu.rs index 2bffa8c..d9751b9 100644 --- a/crates/core/src/menu.rs +++ b/crates/core/src/menu.rs @@ -10,7 +10,7 @@ use gpui::{ ease_out_quint, point, px, relative, }; use gpui_native_icons::{IconRole, icon as icon_glyph}; -use gpui_native_theme::ActiveTheme as _; +use gpui_native_theme::{ActiveTheme as _, POPUP_MAX_HEIGHT}; use crate::common::v_flex; use crate::scroll_area::{ScrollArea, ScrollAxes}; @@ -35,7 +35,7 @@ pub struct MenuItem { handler: Option, disabled: bool, destructive: bool, - checked: Option, + checked: bool, } #[derive(Clone)] @@ -58,7 +58,7 @@ impl MenuItem { handler: None, disabled: false, destructive: false, - checked: None, + checked: false, } } @@ -72,7 +72,7 @@ impl MenuItem { handler: None, disabled: false, destructive: false, - checked: None, + checked: false, } } @@ -114,7 +114,7 @@ impl MenuItem { /// Shows a check slot before the label. pub fn checked(mut self, checked: bool) -> Self { - self.checked = Some(checked); + self.checked = checked; self } @@ -470,7 +470,7 @@ pub(crate) fn anchored_menu_panel( ScrollArea::new("menu-items") .handle(scroll.read(cx).clone()) .axes(ScrollAxes::Vertical) - .max_h(px(320.)) + .max_h(POPUP_MAX_HEIGHT) .child(v_flex().gap_0p5().children(rows)), ) .child( @@ -596,7 +596,7 @@ fn render_item( .child(shortcut), ) }) - .when(checked == Some(true), |row| { + .when(checked, |row| { row.child( icon_glyph(IconRole::Check) .size(chrome.dense_icon_size) diff --git a/crates/core/src/scroll_area.rs b/crates/core/src/scroll_area.rs index 0b5ccb4..bc540c7 100644 --- a/crates/core/src/scroll_area.rs +++ b/crates/core/src/scroll_area.rs @@ -1,17 +1,13 @@ -use std::sync::Arc; -use std::time::Duration; -use std::time::Instant; +//! A scrollable container with an overlay scrollbar. use gpui::prelude::FluentBuilder as _; use gpui::{ - App, Bounds, DispatchPhase, ElementId, Entity, EntityId, InteractiveElement as _, IntoElement, - MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, RenderOnce, ScrollEvent, - ScrollHandle, SharedString, StatefulInteractiveElement as _, Styled, Task, Window, canvas, div, - fill, point, + App, ElementId, InteractiveElement as _, IntoElement, ParentElement, RenderOnce, ScrollHandle, + SharedString, StatefulInteractiveElement as _, Styled, Window, div, }; -use gpui_native_theme::ActiveTheme as _; use crate::common::StyledExt; +use crate::scrollbar::{Scrollbar, ScrollbarVisibility}; /// Which directions the content scrolls in. #[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] @@ -25,108 +21,6 @@ pub enum ScrollAxes { Both, } -/// When an overlay scrollbar shows. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -pub enum ScrollbarVisibility { - /// The running platform's behavior, from the theme. - #[default] - Platform, - Always, - Autohide, -} - -/// How long an autohide thumb stays fully visible before its fade begins. -const AUTOHIDE_DELAY: Duration = Duration::from_millis(700); -/// How long the autohide fade runs. -const AUTOHIDE_FADE: Duration = Duration::from_millis(250); -/// How often the fade repaints while it runs. -const AUTOHIDE_FADE_STEP: Duration = Duration::from_millis(50); - -/// The shared autohide timeline for an overlay scrollbar. -#[derive(Clone)] -pub(crate) struct Autohide { - view: EntityId, - state: Entity, -} - -struct AutohideState { - last_activity: Option, - hide_task: Option>, -} - -impl Autohide { - pub(crate) fn new(key: ElementId, window: &mut Window, cx: &mut App) -> Self { - let view = window.current_view(); - let state = window.use_keyed_state(key, cx, |_, _| AutohideState { - last_activity: None, - hide_task: None, - }); - Self { view, state } - } - - /// Marks interaction. - pub(crate) fn touch(&self, window: &mut Window, cx: &mut App) { - let view = self.view; - let reduce_motion = cx.theme().accessibility.reduce_motion; - - let task = window.spawn(cx, async move |cx| { - if reduce_motion { - cx.background_executor() - .timer(AUTOHIDE_DELAY + AUTOHIDE_FADE) - .await; - cx.update(|_, cx| cx.notify(view)).ok(); - return; - } - - let steps = (AUTOHIDE_FADE.as_millis() / AUTOHIDE_FADE_STEP.as_millis()).max(1); - cx.background_executor().timer(AUTOHIDE_DELAY).await; - - for _ in 0..steps { - cx.update(|_, cx| cx.notify(view)).ok(); - cx.background_executor().timer(AUTOHIDE_FADE_STEP).await; - } - - cx.update(|_, cx| cx.notify(view)).ok(); - }); - - self.state.update(cx, |state, _| { - state.last_activity = Some(Instant::now()); - state.hide_task = Some(task); - }); - } - - /// The thumb's opacity. - pub(crate) fn alpha(&self, cx: &App, dragging: bool) -> f32 { - if dragging { - return 1.0; - } - - let Some(activity) = self.state.read(cx).last_activity else { - return 0.0; - }; - - let idle = activity.elapsed(); - if idle <= AUTOHIDE_DELAY { - return 1.0; - } - - let fade = idle.saturating_sub(AUTOHIDE_DELAY); - if fade >= AUTOHIDE_FADE { - return 0.0; - } - - 1.0 - fade.as_secs_f32() / AUTOHIDE_FADE.as_secs_f32() - } -} - -/// An overlay scrollbar thumb's style for one paint: `None` hides it. -pub(crate) fn thumb_style(color: gpui::Hsla, alpha: f32) -> Option { - (alpha > 0.0).then_some(gpui::Hsla { - a: color.a * alpha, - ..color - }) -} - /// A scrollable container with an overlay scrollbar. #[derive(IntoElement)] pub struct ScrollArea { @@ -196,32 +90,8 @@ impl ScrollArea { } } -/// Names keyed state under the scrollbar's own id. -pub fn scrollbar_state_key(name: &'static str, id: &ElementId) -> ElementId { - ElementId::NamedChild(Arc::new(id.clone()), SharedString::from(name)) -} - -/// Pure thumb geometry for an overlay scrollbar. -pub(crate) fn thumb_geometry( - viewport: Pixels, - content: Pixels, - offset: Pixels, - track: Pixels, - min_thumb: Pixels, -) -> (Pixels, Pixels) { - if content <= viewport || content <= Pixels::ZERO { - return (track, Pixels::ZERO); - } - - let length = (viewport / content * track).max(min_thumb).min(track); - let overflow = content - viewport; - let progress = (-offset / overflow).clamp(0., 1.); - let travel = track - length; - (length, travel * progress) -} - impl RenderOnce for ScrollArea { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { + fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { let mut pane_style = self.style.clone(); pane_style.padding = Default::default(); @@ -252,175 +122,24 @@ impl RenderOnce for ScrollArea { if let Some(handle) = self.handle { let id = self.id.clone(); - let autohide = match self.visibility { - ScrollbarVisibility::Platform => cx.theme().scrollbar.autohide, - ScrollbarVisibility::Always => false, - ScrollbarVisibility::Autohide => true, - }; - wrapper.extend(std::iter::once( - canvas(|bounds, _, _| bounds, { - let handle = handle.clone(); - move |overlay, _, window, cx| { - paint_scrollbar(&handle, &id, autohide, overlay, window, cx); - } - }) - .absolute() - .inset_0() - .into_any_element(), - )); + if self.axes != ScrollAxes::Horizontal { + wrapper.extend(std::iter::once( + Scrollbar::vertical((id.clone(), "v-scrollbar"), handle.clone()) + .visibility(self.visibility) + .into_any_element(), + )); + } + + if self.axes != ScrollAxes::Vertical { + wrapper.extend(std::iter::once( + Scrollbar::horizontal((id, "h-scrollbar"), handle) + .visibility(self.visibility) + .into_any_element(), + )); + } } wrapper } } - -/// Paints the overlay scrollbar thumb. -fn paint_scrollbar( - handle: &ScrollHandle, - key: &ElementId, - autohide: bool, - overlay: Bounds, - window: &mut Window, - cx: &mut App, -) { - let theme = cx.theme(); - let view = window.current_view(); - - let color = theme.colors.scrollbar_thumb; - let width = theme.scrollbar.width; - let inset = theme.scrollbar.inset; - let min_thumb = theme.scrollbar.min_thumb; - - let viewport = handle.bounds(); - let max_offset = handle.max_offset().y; - - if max_offset <= Pixels::ZERO || viewport.size.height <= Pixels::ZERO { - return; - } - - let autohide_state = - autohide.then(|| Autohide::new(scrollbar_state_key("autohide", key), window, cx)); - let drag = window.use_keyed_state(scrollbar_state_key("thumb-drag", key), cx, |_, _| { - Option::<(Pixels, Pixels)>::None - }); - let dragging = drag.read(cx).is_some(); - - let alpha = match (&autohide_state, autohide) { - (Some(state), true) => state.alpha(cx, dragging), - _ => 1.0, - }; - - let track = overlay.size.height - inset * 2.0; - let (thumb_length, thumb_top) = thumb_geometry( - viewport.size.height, - viewport.size.height + max_offset, - handle.offset().y, - track, - min_thumb, - ); - - let thumb_x = overlay.right() - width - inset; - let thumb_bounds = Bounds::from_corners( - point(thumb_x, overlay.top() + inset + thumb_top), - point( - thumb_x + width, - overlay.top() + inset + thumb_top + thumb_length, - ), - ); - - if let Some(color) = thumb_style(color, alpha) { - window.paint_quad(fill(thumb_bounds, color).corner_radii(width / 2.0)); - } - - if let Some(state) = &autohide_state { - window.on_mouse_event({ - let state = state.clone(); - move |event: &ScrollEvent, phase, window, cx| { - if phase != DispatchPhase::Bubble || !overlay.contains(&event.position) { - return; - } - state.touch(window, cx); - } - }); - } - - window.on_mouse_event({ - let handle = handle.clone(); - let drag = drag.clone(); - let autohide_state = autohide_state.clone(); - move |event: &MouseDownEvent, phase, window, cx| { - if phase != DispatchPhase::Bubble - || event.button != gpui::MouseButton::Left - || !thumb_bounds.contains(&event.position) - { - return; - } - - drag.update(cx, |drag, _| { - *drag = Some((event.position.y, handle.offset().y)); - }); - - if let Some(state) = &autohide_state { - state.touch(window, cx); - } - - cx.stop_propagation(); - } - }); - - window.on_mouse_event({ - let drag = drag.clone(); - let autohide_state = autohide_state.clone(); - move |event: &MouseUpEvent, phase, window, cx| { - if phase != DispatchPhase::Bubble || event.button != gpui::MouseButton::Left { - return; - } - - if drag.read(cx).is_none() { - return; - } - - drag.update(cx, |drag, _| { - *drag = None; - }); - - if let Some(state) = &autohide_state { - state.touch(window, cx); - } - } - }); - - window.on_mouse_event({ - let handle = handle.clone(); - let drag = drag.clone(); - let autohide_state = autohide_state.clone(); - move |event: &MouseMoveEvent, phase, window, cx| { - if phase != DispatchPhase::Bubble { - return; - } - - if let Some(state) = &autohide_state - && drag.read(cx).is_some() - { - state.touch(window, cx); - } - - if event.pressed_button != Some(gpui::MouseButton::Left) { - return; - } - - let Some((start_y, start_offset)) = *drag.read(cx) else { - return; - }; - - if max_offset > Pixels::ZERO && track > thumb_length { - let scale = max_offset / (track - thumb_length); - let offset = (start_offset - (event.position.y - start_y) * scale) - .clamp(-max_offset, Pixels::ZERO); - handle.set_offset(point(handle.offset().x, offset)); - cx.notify(view); - } - } - }); -} diff --git a/crates/core/src/scrollbar.rs b/crates/core/src/scrollbar.rs index 6ccbd6a..15b34aa 100644 --- a/crates/core/src/scrollbar.rs +++ b/crates/core/src/scrollbar.rs @@ -1,18 +1,141 @@ //! Overlay scrollbars for scroll surfaces. use std::rc::Rc; +use std::sync::Arc; +use std::time::{Duration, Instant}; use gpui::{ - App, Bounds, DispatchPhase, ElementId, IntoElement, ListState, MouseButton, MouseDownEvent, - MouseMoveEvent, MouseUpEvent, Pixels, Point, RenderOnce, ScrollEvent, ScrollHandle, - Styled as _, UniformListScrollHandle, Window, canvas, fill, point, + App, Bounds, DispatchPhase, ElementId, Entity, EntityId, IntoElement, ListState, MouseButton, + MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, RenderOnce, ScrollEvent, + ScrollHandle, SharedString, Styled as _, Task, UniformListScrollHandle, Window, canvas, fill, + point, }; use gpui_native_theme::ActiveTheme as _; -use crate::scroll_area::{Autohide, scrollbar_state_key, thumb_geometry, thumb_style}; +/// When an overlay scrollbar shows. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum ScrollbarVisibility { + /// The running platform's behavior, from the theme. + #[default] + Platform, + Always, + Autohide, +} -/// Re-exported so callers only need the scrollbar module. -pub use crate::scroll_area::ScrollbarVisibility; +/// How long an autohide thumb stays fully visible before its fade begins. +const AUTOHIDE_DELAY: Duration = Duration::from_millis(700); +/// How long the autohide fade runs. +const AUTOHIDE_FADE: Duration = Duration::from_millis(250); +/// How often the fade repaints while it runs. +const AUTOHIDE_FADE_STEP: Duration = Duration::from_millis(50); + +/// The shared autohide timeline for an overlay scrollbar. +#[derive(Clone)] +struct Autohide { + view: EntityId, + state: Entity, +} + +struct AutohideState { + last_activity: Option, + hide_task: Option>, +} + +impl Autohide { + fn new(key: ElementId, window: &mut Window, cx: &mut App) -> Self { + let view = window.current_view(); + let state = window.use_keyed_state(key, cx, |_, _| AutohideState { + last_activity: None, + hide_task: None, + }); + Self { view, state } + } + + /// Marks interaction. + fn touch(&self, window: &mut Window, cx: &mut App) { + let view = self.view; + let reduce_motion = cx.theme().accessibility.reduce_motion; + + let task = window.spawn(cx, async move |cx| { + if reduce_motion { + cx.background_executor() + .timer(AUTOHIDE_DELAY + AUTOHIDE_FADE) + .await; + cx.update(|_, cx| cx.notify(view)).ok(); + return; + } + + let steps = (AUTOHIDE_FADE.as_millis() / AUTOHIDE_FADE_STEP.as_millis()).max(1); + cx.background_executor().timer(AUTOHIDE_DELAY).await; + + for _ in 0..steps { + cx.update(|_, cx| cx.notify(view)).ok(); + cx.background_executor().timer(AUTOHIDE_FADE_STEP).await; + } + + cx.update(|_, cx| cx.notify(view)).ok(); + }); + + self.state.update(cx, |state, _| { + state.last_activity = Some(Instant::now()); + state.hide_task = Some(task); + }); + } + + /// The thumb's opacity. + fn alpha(&self, cx: &App, dragging: bool) -> f32 { + if dragging { + return 1.0; + } + + let Some(activity) = self.state.read(cx).last_activity else { + return 0.0; + }; + + let idle = activity.elapsed(); + if idle <= AUTOHIDE_DELAY { + return 1.0; + } + + let fade = idle.saturating_sub(AUTOHIDE_DELAY); + if fade >= AUTOHIDE_FADE { + return 0.0; + } + + 1.0 - fade.as_secs_f32() / AUTOHIDE_FADE.as_secs_f32() + } +} + +fn scrollbar_state_key(name: &'static str, id: &ElementId) -> ElementId { + ElementId::NamedChild(Arc::new(id.clone()), SharedString::from(name)) +} + +/// Pure thumb geometry for an overlay scrollbar. +fn thumb_geometry( + viewport: Pixels, + content: Pixels, + offset: Pixels, + track: Pixels, + min_thumb: Pixels, +) -> (Pixels, Pixels) { + if content <= viewport || content <= Pixels::ZERO { + return (track, Pixels::ZERO); + } + + let length = (viewport / content * track).max(min_thumb).min(track); + let overflow = content - viewport; + let progress = (-offset / overflow).clamp(0., 1.); + let travel = track - length; + (length, travel * progress) +} + +/// An overlay scrollbar thumb's style for one paint: `None` hides it. +fn thumb_style(color: gpui::Hsla, alpha: f32) -> Option { + (alpha > 0.0).then_some(gpui::Hsla { + a: color.a * alpha, + ..color + }) +} /// The scroll state a scrollbar drives, implemented for gpui's scroll handles. pub trait ScrollbarHandle: 'static { diff --git a/crates/core/src/separator.rs b/crates/core/src/separator.rs index 0b46652..c53315a 100644 --- a/crates/core/src/separator.rs +++ b/crates/core/src/separator.rs @@ -3,8 +3,8 @@ use std::rc::Rc; use gpui::prelude::FluentBuilder as _; -use gpui::{App, InteractiveElement as _, IntoElement, RenderOnce, Styled as _, Window, div, px}; -use gpui_native_theme::ActiveTheme as _; +use gpui::{App, InteractiveElement as _, IntoElement, RenderOnce, Styled as _, Window, div}; +use gpui_native_theme::{ActiveTheme as _, BORDER_WIDTH}; /// Which axis the rule runs along. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] @@ -69,8 +69,8 @@ impl RenderOnce for Separator { rule.debug_selector(move || selector()) }) .map(|rule| match self.orientation { - SeparatorOrientation::Horizontal => rule.w_full().h(px(1.)), - SeparatorOrientation::Vertical => rule.self_stretch().w(px(1.)), + SeparatorOrientation::Horizontal => rule.w_full().h(BORDER_WIDTH), + SeparatorOrientation::Vertical => rule.self_stretch().w(BORDER_WIDTH), }) } } diff --git a/crates/core/src/slider.rs b/crates/core/src/slider.rs index e246226..b29f11e 100644 --- a/crates/core/src/slider.rs +++ b/crates/core/src/slider.rs @@ -79,12 +79,6 @@ impl Slider { self } - /// Sets the current value; the slider never owns it. - pub fn value(mut self, value: f32) -> Self { - self.value = value; - self - } - /// Renders the slider inert, unfocusable, and dimmed; disabled always wins. pub fn disabled(mut self, disabled: bool) -> Self { self.disabled = disabled; diff --git a/crates/core/src/tooltip.rs b/crates/core/src/tooltip.rs index 26e7d4f..f7af13c 100644 --- a/crates/core/src/tooltip.rs +++ b/crates/core/src/tooltip.rs @@ -1,9 +1,9 @@ use gpui::AppContext as _; use gpui::{ AnyView, App, Bounds, Context, ElementId, Entity, InteractiveElement as _, IntoElement, - ParentElement as _, Pixels, Render, SharedString, Styled as _, Window, canvas, div, point, px, + ParentElement as _, Pixels, Render, SharedString, Styled as _, Window, canvas, div, point, }; -use gpui_native_theme::{ActiveTheme as _, BORDER_WIDTH}; +use gpui_native_theme::{ActiveTheme as _, BORDER_WIDTH, TOOLTIP_MAX_WIDTH, TOOLTIP_OFFSET}; use gpui_native_utils::{anchor_bounds, record_anchor_bounds}; const PANEL_BOUNDS_ID: &str = "tooltip-panel-bounds"; @@ -76,7 +76,7 @@ impl Render for Tooltip { .debug_selector(|| "tooltip-panel".to_string()) .px_2() .py_1() - .max_w(px(360.)) + .max_w(TOOLTIP_MAX_WIDTH) .bg(surface) .border_1() .border_color(border) @@ -89,7 +89,7 @@ impl Render for Tooltip { let (wrapper, panel) = match self.anchor { Some(parent) => { - let origin = window.mouse_position() + point(px(1.), px(1.)); + let origin = window.mouse_position() + point(TOOLTIP_OFFSET, TOOLTIP_OFFSET); let viewport_height = window.viewport_size().height; let height = anchor_bounds( diff --git a/crates/data/src/sidebar.rs b/crates/data/src/sidebar.rs index 8d80bef..4046b60 100644 --- a/crates/data/src/sidebar.rs +++ b/crates/data/src/sidebar.rs @@ -197,16 +197,31 @@ impl RenderOnce for Sidebar { let scroll_area_id = (self.id.clone(), "entries"); - // Entry paint reads the theme through this borrow. + let row_count = entries + .iter() + .filter(|entry| matches!(entry, SidebarEntry::Row { .. })) + .count(); + + let mut row_position = 0usize; let children: Vec = entries .iter() .enumerate() .map(|(index, entry)| { + let is_row = matches!(entry, SidebarEntry::Row { .. }); + if is_row { + row_position += 1; + } + let (position_in_set, size_of_set) = if is_row { + (row_position, row_count) + } else { + (0, 0) + }; render_entry( cx, entry, index, - entries.len(), + position_in_set, + size_of_set, self.selected == Some(index), material, &self.on_change, @@ -258,7 +273,8 @@ fn render_entry( cx: &App, entry: &SidebarEntry, index: usize, - count: usize, + position_in_set: usize, + size_of_set: usize, selected: bool, material: bool, on_change: &Option, @@ -294,8 +310,8 @@ fn render_entry( .rounded(theme.radius.sm) .role(Role::ListItem) .aria_selected(selected) - .aria_position_in_set(index + 1) - .aria_size_of_set(count) + .aria_position_in_set(position_in_set) + .aria_size_of_set(size_of_set) .map(|row| { if material && !*row_disabled { if selected { diff --git a/crates/data/src/tabs.rs b/crates/data/src/tabs.rs index fe13093..51333cb 100644 --- a/crates/data/src/tabs.rs +++ b/crates/data/src/tabs.rs @@ -214,7 +214,6 @@ impl RenderOnce for Tabs { cx, tab, index, - self.selected == index, self.selected, self.variant, &self.on_change, @@ -272,7 +271,6 @@ fn render_tab( cx: &App, tab: Tab, index: usize, - selected: bool, selected_index: usize, variant: TabsVariant, on_change: &Option, @@ -280,6 +278,7 @@ fn render_tab( focus: &gpui::FocusHandle, ) -> AnyElement { let mut tab = tab; + let selected = index == selected_index; let theme = cx.theme(); let metrics = theme.sizes.resolve(Size::default()); diff --git a/crates/data/src/tree.rs b/crates/data/src/tree.rs index 1444a05..d653f82 100644 --- a/crates/data/src/tree.rs +++ b/crates/data/src/tree.rs @@ -166,14 +166,49 @@ impl RenderOnce for Tree { .clone() .unwrap_or_else(|| SharedString::from(self.id.to_string())); + let sibling_sets: Vec<(usize, usize)> = { + let mut sets = vec![(0usize, 0usize); items.len()]; + let mut start = 0usize; + + while start < items.len() { + let depth = items[start].depth; + + let mut end = start + 1; + while end < items.len() && items[end].depth > depth { + end += 1; + } + + let mut position = 0usize; + for index in start..end { + if items[index].depth == depth { + position += 1; + sets[index] = (position, 0); + } + } + + for index in start..end { + if items[index].depth == depth { + sets[index].1 = position; + } + } + + start = end; + } + + sets + }; + let rows: Vec = items .iter() .enumerate() .map(|(index, _item)| { + let (position_in_set, size_of_set) = sibling_sets[index]; render_row( cx, - &items, + &items[index], index, + position_in_set, + size_of_set, self.selected == Some(index), &self.on_change, &self.on_disclosure, @@ -228,21 +263,21 @@ impl RenderOnce for Tree { } } +#[allow(clippy::too_many_arguments)] fn render_row( cx: &App, - items: &[TreeItem], + item: &TreeItem, index: usize, + position_in_set: usize, + size_of_set: usize, selected: bool, on_change: &Option, on_disclosure: &Option, focus: &FocusHandle, ) -> AnyElement { let theme = cx.theme(); - let item = &items[index]; - let count = items.len(); let label_color = selectable_label_color(cx, selected, item.disabled); let disabled = item.disabled; - // One indent step per depth level, on the theme's horizontal padding. let indent = theme.sizes.medium.padding_x * (item.depth as f32 + 1.0); @@ -258,7 +293,8 @@ fn render_row( .role(Role::TreeItem) .aria_selected(selected) .aria_level(item.depth + 1) - .aria_size_of_set(count) + .aria_position_in_set(position_in_set) + .aria_size_of_set(size_of_set) .when(item.expandable, |row| row.aria_expanded(item.expanded)) .map(|row| paint_selectable_row(cx, row, selected, disabled)) .when(!disabled, |row| { diff --git a/crates/editor/src/code_view.rs b/crates/editor/src/code_view.rs index 41d03aa..bd0509d 100644 --- a/crates/editor/src/code_view.rs +++ b/crates/editor/src/code_view.rs @@ -11,7 +11,7 @@ use gpui::{ actions, canvas, div, uniform_list, }; use gpui_native_core::scrollbar::{Scrollbar, ScrollbarHandle, ScrollbarVisibility}; -use gpui_native_theme::{ActiveTheme as _, ColorTokens, Ramps}; +use gpui_native_theme::{ActiveTheme as _, ColorTokens, MONO_ADVANCE_RATIO, Ramps}; use crate::highlight::{HighlightedCode, Language, Role, highlight}; use crate::utils::{gutter_cell, run_style}; @@ -132,11 +132,6 @@ impl CodeViewState { &self.source } - /// The language the source highlights as. - pub fn language(&self) -> Language { - self.language - } - /// The highlighted lines. pub fn code(&self) -> &HighlightedCode { &self.code @@ -209,13 +204,7 @@ impl CodeViewState { (!selected.is_empty()).then(|| selected.join("\n")) } - /// Drops the selection. - pub fn clear_selection(&mut self) { - self.selection = None; - self.selecting = false; - } - - /// Overrides the accessibility label of the view. + // Overrides the accessibility label of the view. pub fn accessibility_label(mut self, label: L) -> Self where L: Into, @@ -235,8 +224,7 @@ impl CodeViewState { text_x: Pixels, ) -> SelectionPoint { let row = (((position.y - bounds.origin.y - scroll_y) / line_height).floor() as isize) - .clamp(0, self.code.lines.len() as isize - 1) - .max(0) as usize; + .clamp(0, self.code.lines.len() as isize - 1) as usize; let chars = self.code.lines[row] .spans @@ -244,9 +232,8 @@ impl CodeViewState { .map(|span| span.text.chars().count()) .sum::(); - let col = (((position.x - text_x) / char_width).round() as isize) - .clamp(0, chars as isize) - .max(0) as usize; + let col = (((position.x - text_x) / char_width).round() as isize).clamp(0, chars as isize) + as usize; SelectionPoint { row, col } } @@ -265,7 +252,7 @@ impl Render for CodeViewState { let mono_font = theme.mono_font; let text_size = theme.sizes.small.text_size; let line_height = text_size * 1.5; - let char_width = text_size * 0.62; + let char_width = text_size * MONO_ADVANCE_RATIO; let root_font_size = theme.root_font_size; let id = self.id.clone(); diff --git a/crates/editor/src/diff_view.rs b/crates/editor/src/diff_view.rs index 1395e75..9c3e980 100644 --- a/crates/editor/src/diff_view.rs +++ b/crates/editor/src/diff_view.rs @@ -13,7 +13,7 @@ use gpui::{ point, uniform_list, }; use gpui_native_core::scrollbar::{Scrollbar, ScrollbarHandle, ScrollbarVisibility}; -use gpui_native_theme::ActiveTheme as _; +use gpui_native_theme::{ActiveTheme as _, MONO_ADVANCE_RATIO}; use similar::{ChangeTag, TextDiff}; use std::ops::Range; @@ -399,7 +399,7 @@ fn render_row( let ramps = theme.ramps; let text_size = theme.sizes.small.text_size; let line_height = text_size * 1.5; - let char_width = text_size * 0.62; + let char_width = text_size * MONO_ADVANCE_RATIO; let frame = div() .debug_selector(move || format!("diff-row-{row_ix}")) diff --git a/crates/editor/src/markdown.rs b/crates/editor/src/markdown.rs index 0e54087..fd8dcba 100644 --- a/crates/editor/src/markdown.rs +++ b/crates/editor/src/markdown.rs @@ -101,8 +101,6 @@ pub enum InlineStyle { #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct MarkdownTableCell { pub spans: Vec, - /// 1-based column position, so renderers can align to the table's columns. - pub column: usize, } /// A table row; the header row precedes the body rows. @@ -293,24 +291,6 @@ struct ListFrame { task: Option, } -/// The table being assembled, when the parser is inside one. -enum TableBuilder { - None, - Body { rows: Vec }, -} - -/// One open code block, accumulating its raw text. -#[derive(Default)] -struct CodeBuilder { - text: String, -} - -impl CodeBuilder { - fn push(&mut self, text: &str) { - self.text.push_str(text); - } -} - /// Accumulates inline spans across inline events. #[derive(Default)] struct InlineBuilder { @@ -403,8 +383,8 @@ fn parse(source: &str, highlight_code: bool) -> Vec { // Whether each open list item already flushed content rows. let mut items: Vec = Vec::new(); let mut quote_depth = 0usize; - let mut table = TableBuilder::None; - let mut code: Option = None; + let mut table: Option> = None; + let mut code: Option = None; // The fence info string arrives on Start, its text follows. let mut code_language: Option = None; @@ -469,7 +449,7 @@ fn parse(source: &str, highlight_code: bool) -> Vec { &mut blocks, quote_depth, ); - code = Some(CodeBuilder::default()); + code = Some(String::new()); if let CodeBlockKind::Fenced(info) = kind { let info = info.trim(); code_language = @@ -484,10 +464,10 @@ fn parse(source: &str, highlight_code: bool) -> Vec { &mut blocks, quote_depth, ); - table = TableBuilder::Body { rows: Vec::new() } + table = Some(Vec::new()) } Tag::TableHead => { - if let TableBuilder::Body { rows } = &mut table { + if let Some(rows) = &mut table { rows.push(MarkdownTableRow { cells: Vec::new(), header: true, @@ -495,7 +475,7 @@ fn parse(source: &str, highlight_code: bool) -> Vec { } } Tag::TableRow => { - if let TableBuilder::Body { rows } = &mut table { + if let Some(rows) = &mut table { rows.push(MarkdownTableRow { cells: Vec::new(), header: false, @@ -583,7 +563,7 @@ fn parse(source: &str, highlight_code: bool) -> Vec { } TagEnd::CodeBlock => { let built = code.take().unwrap_or_default(); - let text = built.text.trim_end_matches('\n'); + let text = built.trim_end_matches('\n'); let language = code_language.take(); let lines = match highlight_code .then(|| language.as_deref().and_then(grammar_language)) @@ -600,8 +580,7 @@ fn parse(source: &str, highlight_code: bool) -> Vec { }); } TagEnd::Table => { - let built = std::mem::replace(&mut table, TableBuilder::None); - if let TableBuilder::Body { rows } = built { + if let Some(rows) = table.take() { blocks.push(MarkdownBlock::Table { table: MarkdownTable { rows }, indent: lists.len(), @@ -611,16 +590,13 @@ fn parse(source: &str, highlight_code: bool) -> Vec { } TagEnd::TableCell => { let spans = inline.finish(); - let TableBuilder::Body { rows } = &mut table else { + let Some(rows) = &mut table else { continue; }; let Some(row) = rows.last_mut() else { continue; }; - row.cells.push(MarkdownTableCell { - spans, - column: row.cells.len() + 1, - }); + row.cells.push(MarkdownTableCell { spans }); } TagEnd::Emphasis | TagEnd::Strong @@ -630,8 +606,8 @@ fn parse(source: &str, highlight_code: bool) -> Vec { _ => {} }, Event::Text(text) => { - if let Some(built) = code.as_mut() { - built.push(&text); + if let Some(buffer) = code.as_mut() { + buffer.push_str(&text); } else { inline.push_text(&text); } @@ -660,9 +636,9 @@ fn parse(source: &str, highlight_code: bool) -> Vec { frame.task = Some(checked); } } - Event::FootnoteReference(label) => inline.push_text(&label), - // HTML and math render nothing in this view. - Event::InlineHtml(_) + // Footnotes are disabled in the parser options. + Event::FootnoteReference(_) + | Event::InlineHtml(_) | Event::Html(_) | Event::InlineMath(_) | Event::DisplayMath(_) => {} diff --git a/crates/editor/tests/markdown.rs b/crates/editor/tests/markdown.rs index 2b55e9d..ce4e412 100644 --- a/crates/editor/tests/markdown.rs +++ b/crates/editor/tests/markdown.rs @@ -305,14 +305,6 @@ fn tables_parse_header_first_with_column_positions() { .map(|cell| spans_text(&cell.spans)) .collect(); assert_eq!(header, vec!["a", "b"]); - assert_eq!( - table.rows[0] - .cells - .iter() - .map(|cell| cell.column) - .collect::>(), - vec![1, 2] - ); assert_eq!(spans_text(&table.rows[2].cells[1].spans), "4"); } diff --git a/crates/layout/Cargo.toml b/crates/layout/Cargo.toml index 490dcb9..0269a3a 100644 --- a/crates/layout/Cargo.toml +++ b/crates/layout/Cargo.toml @@ -20,7 +20,6 @@ default = ["full"] full = ["dock", "resize_handle", "resizable", "shell", "window_controls"] dock = [ "resize_handle", - "resizable", "dep:gpui-native-core", "dep:gpui-native-data", "dep:gpui-native-icons", diff --git a/crates/layout/src/dock.rs b/crates/layout/src/dock.rs index ab4fd5e..5cc8a4c 100644 --- a/crates/layout/src/dock.rs +++ b/crates/layout/src/dock.rs @@ -192,10 +192,8 @@ impl DockState { } /// Registers a panel descriptor, replacing any earlier one with its id. - pub fn register_panel(&mut self, panel: PanelDescriptor, cx: &mut Context) { - if self.panels.insert(panel.id.clone(), panel).is_none() { - cx.notify(); - } + pub fn register_panel(&mut self, panel: PanelDescriptor) { + self.panels.insert(panel.id.clone(), panel); } /// Opens a panel into the active pane and activates its tab. @@ -330,6 +328,17 @@ impl DockState { }; if changed { + // The kept tab may have no right view, show the first remaining one. + self.right_view = self + .locate(id) + .and_then(|(pane_key, _)| self.pane(pane_key)) + .and_then(|pane| { + pane.active.as_deref().and_then(|active| { + pane.tabs.iter().find(|tab| tab.panel.as_ref() == active) + }) + }) + .and_then(|tab| tab.right_view.clone()) + .or_else(|| first_right_view(&self.root)); cx.notify(); } } @@ -354,10 +363,14 @@ impl DockState { } self.detach_pane(pane_key); + if self.active_pane == pane_key { self.active_pane = self.first_pane_key().unwrap_or(0); } + // The closed pane's views are gone; show the first remaining one. + self.right_view = first_right_view(&self.root); + cx.notify(); } @@ -526,6 +539,19 @@ impl DockState { } } + fn pane(&self, key: u64) -> Option<&Pane> { + Self::pane_in(&self.root, key) + } + + fn pane_in(node: &Node, key: u64) -> Option<&Pane> { + match node { + Node::Pane(pane) => (pane.key == key).then_some(pane), + Node::Split(split) => { + Self::pane_in(&split.a, key).or_else(|| Self::pane_in(&split.b, key)) + } + } + } + fn pane_mut(&mut self, key: u64) -> Option<&mut Pane> { Self::pane_mut_in(&mut self.root, key) } @@ -1071,7 +1097,7 @@ fn render_pane( .h(titlebar.height) .items_center() .pl(if titlebar.traffic_light_pad { - px(TRAFFIC_LIGHT_PAD) + TRAFFIC_LIGHT_PAD } else { padding_x }) diff --git a/crates/layout/src/resize_handle.rs b/crates/layout/src/resize_handle.rs index 377cad1..38ca35a 100644 --- a/crates/layout/src/resize_handle.rs +++ b/crates/layout/src/resize_handle.rs @@ -10,11 +10,11 @@ use gpui::{ SharedString, StatefulInteractiveElement as _, Styled as _, Window, canvas, deferred, div, fill, point, px, }; -use gpui_native_theme::{ActiveTheme as _, Size}; +use gpui_native_theme::{ActiveTheme as _, RESIZE_KEYBOARD_STEP, Size}; use gpui_native_utils::{focus_handle, focus_ring}; /// How far one keyboard arrow step moves the seam. -const KEYBOARD_STEP: f32 = 32.0; +const KEYBOARD_STEP: Pixels = RESIZE_KEYBOARD_STEP; type ResizeHandler = Rc; type ResetHandler = Rc; @@ -180,10 +180,10 @@ impl RenderOnce for ResizeHandle { let on_resize = self.on_resize.clone(); move |event: &KeyDownEvent, window, cx| { let delta = match (axis, event.keystroke.key.as_str()) { - (Axis::Horizontal, "left") => Some(px(-KEYBOARD_STEP)), - (Axis::Horizontal, "right") => Some(px(KEYBOARD_STEP)), - (Axis::Vertical, "up") => Some(px(-KEYBOARD_STEP)), - (Axis::Vertical, "down") => Some(px(KEYBOARD_STEP)), + (Axis::Horizontal, "left") => Some(-KEYBOARD_STEP), + (Axis::Horizontal, "right") => Some(KEYBOARD_STEP), + (Axis::Vertical, "up") => Some(-KEYBOARD_STEP), + (Axis::Vertical, "down") => Some(KEYBOARD_STEP), _ => None, }; if let Some(delta) = delta { diff --git a/crates/layout/src/shell.rs b/crates/layout/src/shell.rs index 1eb232b..e718c70 100644 --- a/crates/layout/src/shell.rs +++ b/crates/layout/src/shell.rs @@ -4,8 +4,7 @@ use gpui::prelude::FluentBuilder as _; use gpui::{ AnyElement, App, AppContext as _, Context, ElementId, Entity, InteractiveElement as _, - IntoElement, ParentElement as _, Pixels, RenderOnce, SharedString, Styled as _, Window, div, - px, + IntoElement, ParentElement as _, Pixels, RenderOnce, Styled as _, Window, div, px, }; use gpui_native_core::button::{Button, ButtonVariant}; use gpui_native_core::common::{Sizable as _, h_flex, v_flex}; @@ -108,20 +107,6 @@ impl Side { Side::Right => "right", } } - - fn toggle_icon(self) -> IconRole { - match self { - Side::Left => IconRole::PanelLeft, - Side::Right => IconRole::PanelRight, - } - } - - fn toggle_label(self) -> SharedString { - match self { - Side::Left => SharedString::from("Hide left sidebar"), - Side::Right => SharedString::from("Hide right sidebar"), - } - } } /// The application shell. @@ -204,6 +189,7 @@ impl RenderOnce for Shell { let width = state.read(cx).left_sidebar_width(); layout .child(sidebar_region( + (id.clone(), "left"), width, left.take() .map(|sidebar| sidebar.into_any_element()) @@ -242,6 +228,7 @@ impl RenderOnce for Shell { } })) .child(sidebar_region( + (id.clone(), "right"), width, right.take().unwrap_or_else(|| div().into_any_element()), Side::Right, @@ -262,6 +249,7 @@ impl RenderOnce for Shell { /// One sidebar: a header strip with the hide toggle over the app's content. #[allow(clippy::too_many_arguments)] fn sidebar_region( + id: impl Into, width: Pixels, content: AnyElement, side: Side, @@ -272,6 +260,7 @@ fn sidebar_region( window: &mut Window, cx: &mut App, ) -> AnyElement { + let id = id.into(); let theme = cx.theme(); let background = theme.colors.background; let strip_background = match (side, material) { @@ -280,7 +269,7 @@ fn sidebar_region( }; let strip = h_flex() - .id(SharedString::from(format!("sidebar-strip-{}", side.name()))) + .id((id.clone(), "strip")) .debug_selector(move || format!("sidebar-strip-{}", side.name())) .flex_none() .h(strip_height) @@ -288,40 +277,31 @@ fn sidebar_region( .items_center() .map(|strip| match side { Side::Left => strip.when(cfg!(target_os = "macos"), |strip| { - strip.pl(px(TRAFFIC_LIGHT_PAD)) + strip.pl(TRAFFIC_LIGHT_PAD) }), Side::Right => strip, }) .px(padding_x) .child(titlebar_strip( - div() - .id(SharedString::from(format!( - "sidebar-strip-drag-{}", - side.name() - ))) - .flex_1() - .self_stretch(), + div().id((id.clone(), "strip-drag")).flex_1().self_stretch(), window, cx, )) .map(|strip| match side { Side::Left => strip.child( - Button::new(SharedString::from(format!( - "sidebar-toggle-{}", - side.name() - ))) - .icon(side.toggle_icon()) - .variant(ButtonVariant::Ghost) - .small() - .accessibility_label(side.toggle_label()) - .on_click({ - let state = state.clone(); - move |_, _, cx| { - state.update(cx, |state, cx| state.set_left_sidebar_visible(false, cx)) - } - }), + Button::new((id, "sidebar-toggle")) + .icon(IconRole::PanelLeft) + .variant(ButtonVariant::Ghost) + .small() + .accessibility_label("Hide left sidebar") + .on_click({ + let state = state.clone(); + move |_, _, cx| { + state.update(cx, |state, cx| state.set_left_sidebar_visible(false, cx)) + } + }), ), - Side::Right => strip.child(WindowControls::new("sidebar-window-controls")), + Side::Right => strip.child(WindowControls::new((id, "window-controls"))), }); v_flex() diff --git a/crates/layout/src/window_controls.rs b/crates/layout/src/window_controls.rs index d4deaae..3560ff4 100644 --- a/crates/layout/src/window_controls.rs +++ b/crates/layout/src/window_controls.rs @@ -23,13 +23,9 @@ use gpui::Decorations; use gpui::{SharedString, px, rgba}; /// macOS traffic-lights zone: the pad a leading titlebar strip reserves. -pub const TRAFFIC_LIGHT_PAD: f32 = 78.0; -/// The width of a Windows caption button. +pub use gpui_native_theme::TRAFFIC_LIGHT_PAD; #[cfg(target_os = "windows")] -const CAPTION_WIDTH: f32 = 36.0; -/// Windows paints its caption close hover red in every theme. -#[cfg(target_os = "windows")] -const WINDOWS_CLOSE_HOVER: (f32, f32, f32) = (232.0 / 255.0, 17.0 / 255.0, 32.0 / 255.0); +use gpui_native_theme::{WINDOWS_CAPTION_WIDTH as CAPTION_WIDTH, WINDOWS_CLOSE_HOVER_HEX}; /// The window controls for the trailing end of a titlebar strip. #[derive(IntoElement)] @@ -150,7 +146,7 @@ impl RenderOnce for WindowControls { } /// Double-click on a titlebar strip maximizes where the platform does not do it. -pub fn maximize_on_double_click() -> impl Fn(&ClickEvent, &mut Window, &mut App) + 'static { +fn maximize_on_double_click() -> impl Fn(&ClickEvent, &mut Window, &mut App) + 'static { move |event, window, _cx| { if event.click_count() != 2 { return; @@ -210,16 +206,9 @@ fn caption_button( ) -> AnyElement { let theme = cx.theme(); let glyph_size = theme.sizes.small.caption_size; - let (close_r, close_g, close_b) = WINDOWS_CLOSE_HOVER; - let close_hover = rgba(( - (close_r * 255.0) as u8, - (close_g * 255.0) as u8, - (close_b * 255.0) as u8, - 0xff, - )); let hover = if close { - close_hover + rgba(WINDOWS_CLOSE_HOVER_HEX) } else { theme.colors.muted_hover }; diff --git a/crates/layout/src/window_frame.rs b/crates/layout/src/window_frame.rs index bc0c3d5..b123aa3 100644 --- a/crates/layout/src/window_frame.rs +++ b/crates/layout/src/window_frame.rs @@ -7,12 +7,8 @@ use gpui::{ IntoElement, MouseButton, ParentElement as _, Pixels, Point, ResizeEdge, Size, Styled as _, Tiling, Window, canvas, div, point, px, size, }; -use gpui_native_theme::ActiveTheme as _; +use gpui_native_theme::{ActiveTheme as _, BORDER_WIDTH, WINDOW_RESIZE_INSET}; -/// The width of the band inside the window's edge that resizes it. -const RESIZE_INSET: f32 = 4.0; -/// The hairline around the content. -const BORDER_WIDTH: f32 = 1.0; /// How much of the shadow's strength survives losing window focus. const INACTIVE_SHADOW_OPACITY: f32 = 0.7; @@ -28,7 +24,7 @@ pub fn frame(child: impl IntoElement, window: &mut Window, cx: &App) -> Div { tiling }; - let inset = px(RESIZE_INSET); + let inset = WINDOW_RESIZE_INSET; let border = cx.theme().colors.border; let radius = cx.theme().radius.overlay; @@ -75,10 +71,10 @@ pub fn frame(child: impl IntoElement, window: &mut Window, cx: &App) -> Div { .size_full() .overflow_hidden() .border_color(border) - .when(!tiling.top, |surface| surface.border_t(px(BORDER_WIDTH))) - .when(!tiling.bottom, |surface| surface.border_b(px(BORDER_WIDTH))) - .when(!tiling.left, |surface| surface.border_l(px(BORDER_WIDTH))) - .when(!tiling.right, |surface| surface.border_r(px(BORDER_WIDTH))) + .when(!tiling.top, |surface| surface.border_t(BORDER_WIDTH)) + .when(!tiling.bottom, |surface| surface.border_b(BORDER_WIDTH)) + .when(!tiling.left, |surface| surface.border_l(BORDER_WIDTH)) + .when(!tiling.right, |surface| surface.border_r(BORDER_WIDTH)) .when(!(tiling.top || tiling.left), |surface| { surface.rounded_tl(radius) }) diff --git a/crates/layout/tests/dock.rs b/crates/layout/tests/dock.rs index 270162e..d96a3bc 100644 --- a/crates/layout/tests/dock.rs +++ b/crates/layout/tests/dock.rs @@ -112,9 +112,9 @@ fn drag(visual: &mut VisualTestContext, from: gpui::Point, to: gpui::Poi fn register(dock: &Entity, tags: &[&'static str], cx: &mut TestAppContext) { cx.update(|cx| { - dock.update(cx, |dock, cx| { + dock.update(cx, |dock, _cx| { for tag in tags { - dock.register_panel(descriptor(tag), cx); + dock.register_panel(descriptor(tag)); } }); }); @@ -492,9 +492,9 @@ fn activating_a_panel_with_a_right_view_opens_the_sidebar() { let mut cx = TestAppContext::single(); let (dock, opened, mut visual) = sidebar_window(&mut cx); cx.update(|cx| { - dock.update(cx, |dock, cx| { - dock.register_panel(descriptor_with_right_sidebar("overview"), cx); - dock.register_panel(descriptor("inbox"), cx); + dock.update(cx, |dock, _cx| { + dock.register_panel(descriptor_with_right_sidebar("overview")); + dock.register_panel(descriptor("inbox")); }); }); @@ -519,9 +519,9 @@ fn closing_the_displayed_tab_falls_back_to_another_right_view() { let mut cx = TestAppContext::single(); let (dock, opened, mut visual) = sidebar_window(&mut cx); cx.update(|cx| { - dock.update(cx, |dock, cx| { - dock.register_panel(descriptor_with_right_sidebar("overview"), cx); - dock.register_panel(descriptor_with_right_sidebar("explore"), cx); + dock.update(cx, |dock, _cx| { + dock.register_panel(descriptor_with_right_sidebar("overview")); + dock.register_panel(descriptor_with_right_sidebar("explore")); }); }); open(&mut visual, &dock, "overview"); diff --git a/crates/layout/tests/window_controls.rs b/crates/layout/tests/window_controls.rs index dc2c57c..d513841 100644 --- a/crates/layout/tests/window_controls.rs +++ b/crates/layout/tests/window_controls.rs @@ -74,9 +74,7 @@ fn the_pill_bar_pads_for_traffic_lights_when_it_leads_the_window() { let mut cx = TestAppContext::single(); let (dock, mut visual) = dock_window(false, false, &mut cx); cx.update(|cx| { - dock.update(cx, |dock, cx| { - dock.register_panel(descriptor("overview"), cx) - }); + dock.update(cx, |dock, _cx| dock.register_panel(descriptor("overview"))); }); open(&mut visual, &dock, "overview"); @@ -87,9 +85,7 @@ fn the_pill_bar_pads_for_traffic_lights_when_it_leads_the_window() { let (dock, mut visual) = dock_window(false, true, &mut cx); let _ = &dock; cx.update(|cx| { - dock.update(cx, |dock, cx| { - dock.register_panel(descriptor("overview"), cx) - }); + dock.update(cx, |dock, _cx| dock.register_panel(descriptor("overview"))); }); open(&mut visual, &dock, "overview"); @@ -101,11 +97,11 @@ fn the_pill_bar_pads_for_traffic_lights_when_it_leads_the_window() { // The bare bar keeps the standard leading padding; the padded bar clears // the traffic lights. assert!( - bare_inset < px(TRAFFIC_LIGHT_PAD), + bare_inset < TRAFFIC_LIGHT_PAD, "the bare bar must not reserve the traffic-light pad (bare {bare_inset:?})" ); assert!( - padded_inset >= px(TRAFFIC_LIGHT_PAD) - px(1.), + padded_inset >= TRAFFIC_LIGHT_PAD - px(1.), "the pad must clear the traffic lights (bare {bare_inset:?}, padded {padded_inset:?})" ); } @@ -115,9 +111,7 @@ fn window_controls_render_only_when_the_flag_is_set() { let mut cx = TestAppContext::single(); let (dock, mut visual) = dock_window(false, false, &mut cx); cx.update(|cx| { - dock.update(cx, |dock, cx| { - dock.register_panel(descriptor("overview"), cx) - }); + dock.update(cx, |dock, _cx| dock.register_panel(descriptor("overview"))); }); open(&mut visual, &dock, "overview"); @@ -138,9 +132,7 @@ fn the_flag_hosts_the_platform_controls_in_the_bar() { let mut cx = TestAppContext::single(); let (dock, mut visual) = dock_window(true, false, &mut cx); cx.update(|cx| { - dock.update(cx, |dock, cx| { - dock.register_panel(descriptor("overview"), cx) - }); + dock.update(cx, |dock, _cx| dock.register_panel(descriptor("overview"))); }); open(&mut visual, &dock, "overview"); diff --git a/crates/theme/src/lib.rs b/crates/theme/src/lib.rs index 4bac397..e133963 100644 --- a/crates/theme/src/lib.rs +++ b/crates/theme/src/lib.rs @@ -23,9 +23,39 @@ pub use system::{AccessibilityPreferences, PartialPalette, SourceKind, SystemThe use gpui::{App, Global, Hsla, Pixels, WindowAppearance, WindowBackgroundAppearance, px}; use std::time::Duration; -/// The hairline width of the control border every button and field paints; -/// sized outside the stated metrics so control sizes match across platforms. +/// The hairline width of the control border every button and field paints. pub const BORDER_WIDTH: Pixels = px(1.0); +/// The tallest an anchored popup's list may grow. +pub const POPUP_MAX_HEIGHT: Pixels = px(320.0); +/// The widest a tooltip may grow. +pub const TOOLTIP_MAX_WIDTH: Pixels = px(360.0); +/// How far a tooltip sits from the pointer. +pub const TOOLTIP_OFFSET: Pixels = px(1.0); +/// The caret's thickness. +pub const CARET_WIDTH: Pixels = px(1.5); +/// The IME preedit underline's thickness. +pub const UNDERLINE_WIDTH: Pixels = px(1.0); +/// How far one wheel line scrolls, as a multiple of the line height. +pub const WHEEL_LINE_HEIGHT: f32 = 20.0; +/// How far one keyboard arrow step moves a resize seam. +pub const RESIZE_KEYBOARD_STEP: Pixels = px(32.0); +/// The inset around a window's edge that starts a resize gesture. +pub const WINDOW_RESIZE_INSET: Pixels = px(4.0); +/// The pad macOS reserves for its traffic lights at a strip's leading edge. +pub const TRAFFIC_LIGHT_PAD: Pixels = px(78.0); +/// The width of a Windows caption button. +pub const WINDOWS_CAPTION_WIDTH: Pixels = px(36.0); +/// Windows paints its caption close-button hover red in every theme. +pub const WINDOWS_CLOSE_HOVER_HEX: u32 = 0xE81120FF; +/// The monospace advance estimate as a share of the text size. +pub const MONO_ADVANCE_RATIO: f32 = 0.62; +/// How much a pixel avatar's tint washes over its background. +pub const PIXEL_AVATAR_TINT_ALPHA: f32 = 0.16; +/// Pixel avatar cell lightness multipliers. +pub const PIXEL_AVATAR_LIGHT_LIFT: f32 = 1.6; +pub const PIXEL_AVATAR_LIGHT_CAP: f32 = 0.95; +pub const PIXEL_AVATAR_DARK_FACTOR: f32 = 0.45; +pub const PIXEL_AVATAR_DARK_FLOOR: f32 = 0.18; /// Light or dark, resolved from the system appearance. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Deserialize)] diff --git a/docs/component-audit.md b/docs/component-audit.md index 9667a60..3115fab 100644 --- a/docs/component-audit.md +++ b/docs/component-audit.md @@ -65,6 +65,8 @@ gpui scopes `use_keyed_state` by the element-id path; a `RenderOnce` renders und All verified by workspace-wide greps with **zero call sites** outside their own definitions/tests. +> **Re-checked 2026-11-09** (post-P1): several entries below are superseded by the policy "visual/a11y surface stays even when dead" — see the current P2 plan at the bottom of this document for the authoritative delete/keep split. Notably kept: `PanelDescriptor::icon`, `DropdownMenu::icon()`/`.tab_stop()`, `Alert::icon()`/`.no_icon()`, `markdown plain_code()`, the `accessibility_label` convention, `ResizeHandle::disabled()`, `Resizable::sizes()`/`set_sizes()` (test fixture), `Separator::orientation()`. Still deleted: everything non-UI in the list. Already resolved by review passes: `Tab::lead` (deleted), `ComboboxChanged` re-verified still present. + | Item | Location | Note | |---|---|---| | `Slider::value()` | `slider.rs:83-86` | `new` already takes `value` | @@ -175,9 +177,50 @@ Rule: `crates/theme` owns every color and metric — no raw `rgb`/`rgba`/`hsla` --- -## Recommended fix order (before migrating `signed`) +## Progress -1. **P0 — shared keyed state sweep** (findings 1-4): ~15 lines total; add a two-sibling regression test per component, since single-instance tests provably mask this class. -2. **P0 — user-facing bugs**: IME UTF-16 conversion (5), combobox chevron (6), palette z-order (7), code_view click (8) + incremental misuse (9), resize capture loss (10), tab element ids (14). -3. **P1 — performance**: registry (11), caret blink (12), widest_row (13), the `debug_selector` batch, the per-frame clone batch. -4. **P2 — deletions**: the param/dead-API table and the over-engineering list — this is where "no over-engineering" actually gets enforced, and it shrinks the API surface `signed` will migrate onto. +### P0 — done (commit `73f7dc3`, reviewed) + +- Shared keyed-state sweep (findings 1-4): all `use_keyed_state` keys carry the instance id; `Scrollbar::vertical/horizontal(id, handle)` now require an id, `.key()` deleted. +- Correctness bugs (5, 6, 7, 8, 9, 10, 14): IME UTF-16 conversion fixed, combobox chevron lists all options, palette modal deferred + dead bounds machinery removed, code_view click honors scroll, tree-sitter incremental misuse deleted (`highlight_with_tree` folded into `highlight`), resize drag ends on capture loss, `Tab::new(key, label)` with stable element ids. +- Regression tests added: two-sibling independence for slider / dropdown / scrollbar, resize capture-loss, multibyte UTF-16 conversions. + +### P1 — done (in working tree, uncommitted) + +- Registry (11): keyed by `(WindowId, ElementId)`, mutated in place via `cx.default_global` — no whole-map clone, no cross-window bleed. `anchor_bounds` takes the window id now. +- Caret blink (12): `with_max_fps(4.0)`; `field_bench` updated to probe the capped contract (executor-timer redraw, no next-frame demand). +- widest_row (13): cached in `set_rows` (called by `new`/`set_versions`). +- `debug_selector` batch: all eager formats moved into closures (switch, field ×3, combobox ×2, command_palette ×2, tabs ×2, sidebar, code_view, markdown, dock ×2 reusing the `.id()` format). +- Per-frame clone batch: segmented options are `Rc<[SegmentedOption]>` (closures capture bumps; `selectable` Vec → index math), select trigger text computed in render, menu/dropdown items passed as `Rc<[MenuItem]>`, alert borrows entries, code_view borrows the row `Line`. +- Skipped deliberately: the caret animation-id `format!` (removing it needs id plumbing through `InputState` for a ~30-byte alloc — over-engineering). +- Decision: registry stays a gpui `Global`, not a `OnceLock`. `WindowId`s come from a per-`App` slotmap, so a process-static map would leak state across tests (every `TestAppContext` mints the same early window ids); a Global dies with its `App` and needs no locking. + +Validation after P1: `cargo check --workspace --all-targets` clean, `cargo test --workspace` 64 binaries 0 failures, `cargo fmt --check` clean. + +### P2 — done (in working tree, uncommitted) + +Re-verified the whole P2 list against the tree first. Policy recorded in the plan below: visual/a11y surface stays even when dead; only non-UI dead code was deleted. + +**1a — deleted (non-UI dead API):** `Slider::value()`, `ComboboxChanged` + its `EventEmitter` impl + emit, `CodeView::language()`, `CodeView::clear_selection()`, `Input::submit_on()` getter, `MarkdownTableCell.column` (+ bookkeeping + test assertion), `Side::toggle_icon`/`toggle_label` (left values inlined at the call site), `render_tab`'s duplicate `selected` param (derived from `selected_index`), `register_panel`'s first-insert-only notify and its `cx` parameter (all call sites updated), `Menu checked` field → plain `bool`, `refilter` → `()`. Visibility narrowed: `scrollbar_state_key` → `pub(crate)`, `maximize_on_double_click` → private, `into_bindings` → `pub(crate)`. + +**1b — kept (UI/a11y surface, dead is fine):** `PanelDescriptor::icon`, `DropdownMenu::icon()`/`.tab_stop()`, `Alert::icon()`/`.no_icon()`, `markdown plain_code()`, the whole `accessibility_label` convention, `ResizeHandle::disabled()`, `Resizable::sizes()`/`set_sizes()`, `Separator::orientation()`, `data` closure `&mut Window` params. One remaining fix moved to follow-ups: `Segmented`'s unlabeled aria fallback announcing internal ids. + +**2 — over-engineering deletions:** dialog's `BoundsCell` + capture canvas + bounds check deleted (panel `stop_propagation` is the one mechanism); scroll_area's ~150-line drifted painter deleted — it now composes the real `Scrollbar` (`ScrollbarVisibility`, `Autohide`, and the geometry helpers moved into `scrollbar.rs`), and `ScrollAxes::Both` now renders a horizontal thumb too, resolving the promised-but-unimplemented `Horizontal`; input's `Rc>>` → plain `Option` field, both always-`Some` handler `Option`s → non-optional (two `expect`s deleted), `caret_epoch_started: Option` → `Instant`; markdown's `CodeBuilder` and `TableBuilder` deleted (`String` / `Option>`), the unreachable footnote arm folded into the render-nothing group; avatar's `PixelAvatar.style` + `Styled` impl + duplicate clipping deleted; combobox `refilter` clamps the highlight and the identity round-trip bounds check became `highlight.filter(...)`; code_view's two no-op `.max(0)`s deleted; layout `Cargo.toml`: `dock` now depends on `resize_handle` directly instead of `resizable`. + +**3 — theme-token hoists:** new theme consts: `POPUP_MAX_HEIGHT` (the three `px(320.)` popup sites), `TOOLTIP_MAX_WIDTH`/`TOOLTIP_OFFSET`, `CARET_WIDTH`, `UNDERLINE_WIDTH`, `WHEEL_LINE_HEIGHT`, `RESIZE_KEYBOARD_STEP`, `WINDOW_RESIZE_INSET`, `TRAFFIC_LIGHT_PAD` (moved out of window_controls; dock/shell/tests import it from the theme), `WINDOWS_CAPTION_WIDTH`, `WINDOWS_CLOSE_HOVER_HEX`, `MONO_ADVANCE_RATIO` (the duplicated `0.62` estimate), `PIXEL_AVATAR_*` tint/shade factors. Button's Ghost border now paints `transparent_black()` instead of raw `hsla(0,0,0,0)` (kept painted for uniform control metrics), dropdown fallback uses `transparent_black()`, separator hairline uses `BORDER_WIDTH`. + +**4 — smaller cleanups:** input early-returns when a cursor/selection/wheel change is a no-op (no notify, no IME re-query, no blink-epoch reset); IME pre-edit steps merge into the previous undo entry unconditionally while a composition is active; `line.paint` errors logged instead of `.ok()`; `caret_visible` takes `&self`; `Alerts::clear` runs every entry's `on_dismiss` handler (matches `Toasts::clear`); dock `close_others`/`close_pane` now reconcile the displayed right view (`close_panel` already did); combobox arms row 0 unconditionally on open, so empty results show "No matches" instead of closing (palette parity); palette rows keyed `("command", command.id)` — stable domain ids, not per-keystroke indexes; shell element ids composed from the shell id (`(id, "strip")`, `(id, "strip-drag")`, `(id, "sidebar-toggle")`, `(id, "window-controls")`); sidebar a11y sets count sibling rows only (headers excluded); tree a11y position/size computed among same-depth siblings in one pass. + +Validation after P2: `cargo fmt --check` clean, `cargo check --workspace --all-targets` 0 errors/0 warnings, `cargo test --workspace` 64 binaries 0 failures. + +--- + +## Next step — `signed` migration + +The audit backlog (P0-P2) is complete. Remaining small follow-ups, none blocking: `Segmented`'s unlabeled aria fallback; `markdown plain_code()`'s double parse if a consumer ever uses it; `Separator::orientation()` fold if strictness is ever wanted. + +Kickoff: + +1. Clone `signed` (git.reya.info/reya/signed) beside this workspace. +2. Inventory its views against the component list; mark each usage as: direct fit / needs a small adapter / needs a new component. +3. Bring missing components in-tree here (per repo rules: components are written here, not ported), then migrate view-by-view. diff --git a/examples/gallery/src/main.rs b/examples/gallery/src/main.rs index 23dbe46..d7663a8 100644 --- a/examples/gallery/src/main.rs +++ b/examples/gallery/src/main.rs @@ -424,294 +424,204 @@ impl Gallery { }) .into() }), - cx, ); - dock.register_panel( - PanelDescriptor::new("button", "Button", |_, cx| { - cx.new(|_| ButtonDemo { clicks: 0 }).into() - }), - cx, - ); - dock.register_panel( - PanelDescriptor::new("input", "Input", |_, cx| cx.new(InputDemo::new).into()), - cx, - ); - dock.register_panel( - PanelDescriptor::new("field", "Field", |window, cx| { - cx.new(|cx| FieldDemo::new(window, cx)).into() - }), - cx, - ); - dock.register_panel( - PanelDescriptor::new("combobox", "Combobox", |window, cx| { - cx.new(|cx| ComboboxDemo::new(window, cx)).into() - }), - cx, - ); - dock.register_panel( - PanelDescriptor::new("status-dot", "Status dot", |_, cx| { - cx.new(|_| StatusDotDemo).into() - }), - cx, - ); - dock.register_panel( - PanelDescriptor::new("checkbox", "Checkbox", |_, cx| { - cx.new(|_| CheckboxDemo { checked: false }).into() - }), - cx, - ); - dock.register_panel( - PanelDescriptor::new("radio", "Radio", |_, cx| { - cx.new(|_| RadioDemo { - plan: SharedString::from("starter"), + dock.register_panel(PanelDescriptor::new("button", "Button", |_, cx| { + cx.new(|_| ButtonDemo { clicks: 0 }).into() + })); + dock.register_panel(PanelDescriptor::new("input", "Input", |_, cx| { + cx.new(InputDemo::new).into() + })); + dock.register_panel(PanelDescriptor::new("field", "Field", |window, cx| { + cx.new(|cx| FieldDemo::new(window, cx)).into() + })); + dock.register_panel(PanelDescriptor::new( + "combobox", + "Combobox", + |window, cx| cx.new(|cx| ComboboxDemo::new(window, cx)).into(), + )); + dock.register_panel(PanelDescriptor::new("status-dot", "Status dot", |_, cx| { + cx.new(|_| StatusDotDemo).into() + })); + dock.register_panel(PanelDescriptor::new("checkbox", "Checkbox", |_, cx| { + cx.new(|_| CheckboxDemo { checked: false }).into() + })); + dock.register_panel(PanelDescriptor::new("radio", "Radio", |_, cx| { + cx.new(|_| RadioDemo { + plan: SharedString::from("starter"), + }) + .into() + })); + dock.register_panel(PanelDescriptor::new("switch", "Switch", |_, cx| { + cx.new(|_| SwitchDemo { checked: true }).into() + })); + dock.register_panel(PanelDescriptor::new("toggle", "Toggle", |_, cx| { + cx.new(|_| ToggleDemo { pressed: false }).into() + })); + dock.register_panel(PanelDescriptor::new("slider", "Slider", |_, cx| { + cx.new(|_| SliderDemo { volume: 40.0 }).into() + })); + dock.register_panel(PanelDescriptor::new("segmented", "Segmented", |_, cx| { + cx.new(|_| SegmentedDemo { + filter: ExploreFilter::default(), + }) + .into() + })); + dock.register_panel(PanelDescriptor::new("select", "Select", |_, cx| { + cx.new(|_| SelectDemo { + strategy: Some(Strategy::Curated), + }) + .into() + })); + dock.register_panel(PanelDescriptor::new("link", "Link", |_, cx| { + cx.new(|_| LinkDemo { clicks: 0 }).into() + })); + dock.register_panel(PanelDescriptor::new("clipboard", "Clipboard", |_, cx| { + cx.new(|_| ClipboardDemo).into() + })); + dock.register_panel(PanelDescriptor::new("badge", "Badge", |_, cx| { + cx.new(|_| BadgeDemo).into() + })); + dock.register_panel(PanelDescriptor::new("avatar", "Avatar", |_, cx| { + cx.new(|_| AvatarDemo).into() + })); + dock.register_panel(PanelDescriptor::new("progress", "Progress", |_, cx| { + cx.new(|_| ProgressDemo).into() + })); + dock.register_panel(PanelDescriptor::new("spinner", "Spinner", |_, cx| { + cx.new(|_| SpinnerDemo).into() + })); + dock.register_panel(PanelDescriptor::new("list", "List", |_, cx| { + cx.new(|_| ListDemo { selected: None }).into() + })); + dock.register_panel(PanelDescriptor::new("tree", "Tree", |_, cx| { + cx.new(|_| TreeDemo { + nodes: TreeDemo::demo(), + selected: None, + }) + .into() + })); + dock.register_panel(PanelDescriptor::new("tabs", "Tabs", |_, cx| { + cx.new(|_| TabsDemo { + selected: 0, + pill_tabs: vec![ + SharedString::from("Explorer"), + SharedString::from("Syncing"), + SharedString::from("Blame"), + ], + pill_selected: 0, + }) + .into() + })); + dock.register_panel(PanelDescriptor::new("alert", "Alert", { + let alerts = alerts.clone(); + move |_, cx| { + cx.new(|_| AlertDemo { + alerts: alerts.clone(), + dismissed: false, }) .into() - }), - cx, - ); - dock.register_panel( - PanelDescriptor::new("switch", "Switch", |_, cx| { - cx.new(|_| SwitchDemo { checked: true }).into() - }), - cx, - ); - dock.register_panel( - PanelDescriptor::new("toggle", "Toggle", |_, cx| { - cx.new(|_| ToggleDemo { pressed: false }).into() - }), - cx, - ); - dock.register_panel( - PanelDescriptor::new("slider", "Slider", |_, cx| { - cx.new(|_| SliderDemo { volume: 40.0 }).into() - }), - cx, - ); - dock.register_panel( - PanelDescriptor::new("segmented", "Segmented", |_, cx| { - cx.new(|_| SegmentedDemo { - filter: ExploreFilter::default(), + } + })); + dock.register_panel(PanelDescriptor::new("toast", "Toast", { + let toasts = toasts.clone(); + move |_, cx| { + cx.new(|_| ToastDemo { + toasts: toasts.clone(), }) .into() - }), - cx, - ); - dock.register_panel( - PanelDescriptor::new("select", "Select", |_, cx| { - cx.new(|_| SelectDemo { - strategy: Some(Strategy::Curated), + } + })); + dock.register_panel(PanelDescriptor::new("dialog", "Dialog", { + let gallery = gallery.clone(); + move |_, cx| { + cx.new(|_| DialogDemo { + gallery: gallery.clone(), }) .into() - }), - cx, - ); - dock.register_panel( - PanelDescriptor::new("link", "Link", |_, cx| { - cx.new(|_| LinkDemo { clicks: 0 }).into() - }), - cx, - ); - dock.register_panel( - PanelDescriptor::new("clipboard", "Clipboard", |_, cx| { - cx.new(|_| ClipboardDemo).into() - }), - cx, - ); - dock.register_panel( - PanelDescriptor::new("badge", "Badge", |_, cx| cx.new(|_| BadgeDemo).into()), - cx, - ); - dock.register_panel( - PanelDescriptor::new("avatar", "Avatar", |_, cx| cx.new(|_| AvatarDemo).into()), - cx, - ); - dock.register_panel( - PanelDescriptor::new("progress", "Progress", |_, cx| { - cx.new(|_| ProgressDemo).into() - }), - cx, - ); - dock.register_panel( - PanelDescriptor::new("spinner", "Spinner", |_, cx| cx.new(|_| SpinnerDemo).into()), - cx, - ); - dock.register_panel( - PanelDescriptor::new("list", "List", |_, cx| { - cx.new(|_| ListDemo { selected: None }).into() - }), - cx, - ); - dock.register_panel( - PanelDescriptor::new("tree", "Tree", |_, cx| { - cx.new(|_| TreeDemo { - nodes: TreeDemo::demo(), - selected: None, - }) - .into() - }), - cx, - ); - dock.register_panel( - PanelDescriptor::new("tabs", "Tabs", |_, cx| { - cx.new(|_| TabsDemo { - selected: 0, - pill_tabs: vec![ - SharedString::from("Explorer"), - SharedString::from("Syncing"), - SharedString::from("Blame"), - ], - pill_selected: 0, - }) - .into() - }), - cx, - ); - dock.register_panel( - PanelDescriptor::new("alert", "Alert", { - let alerts = alerts.clone(); - move |_, cx| { - cx.new(|_| AlertDemo { - alerts: alerts.clone(), - dismissed: false, - }) - .into() - } - }), - cx, - ); - dock.register_panel( - PanelDescriptor::new("toast", "Toast", { - let toasts = toasts.clone(); - move |_, cx| { - cx.new(|_| ToastDemo { - toasts: toasts.clone(), - }) - .into() - } - }), - cx, - ); - dock.register_panel( - PanelDescriptor::new("dialog", "Dialog", { - let gallery = gallery.clone(); - move |_, cx| { - cx.new(|_| DialogDemo { - gallery: gallery.clone(), - }) - .into() - } - }), - cx, - ); - dock.register_panel( - PanelDescriptor::new("command-palette", "Command palette", { + } + })); + dock.register_panel(PanelDescriptor::new( + "command-palette", + "Command palette", + { let palette = palette.clone(); move |_, cx| { cx.new(|cx| CommandPaletteDemo::new(gallery.clone(), palette.clone(), cx)) .into() } - }), - cx, - ); - dock.register_panel( - PanelDescriptor::new("menu", "Menu", |_, cx| { - cx.new(|_| MenuDemo { open: false }).into() - }), - cx, - ); - dock.register_panel( - PanelDescriptor::new("dropdown-menu", "Dropdown menu", |_, cx| { + }, + )); + dock.register_panel(PanelDescriptor::new("menu", "Menu", |_, cx| { + cx.new(|_| MenuDemo { open: false }).into() + })); + dock.register_panel(PanelDescriptor::new( + "dropdown-menu", + "Dropdown menu", + |_, cx| { cx.new(|_| DropdownMenuDemo { open_menu: None, status: None, }) .into() - }), - cx, - ); - dock.register_panel( - PanelDescriptor::new("context-menu", "Context menu", |_, cx| { + }, + )); + dock.register_panel(PanelDescriptor::new( + "context-menu", + "Context menu", + |_, cx| { cx.new(|_| ContextMenuDemo { action: SharedString::from("Right-click the card"), }) .into() - }), - cx, - ); - dock.register_panel( - PanelDescriptor::new("popover", "Popover", |_, cx| { - cx.new(|_| PopoverDemo { open: false }).into() - }), - cx, - ); - dock.register_panel( - PanelDescriptor::new("tooltip", "Tooltip", |_, cx| cx.new(|_| TooltipDemo).into()), - cx, - ); - dock.register_panel( - PanelDescriptor::new("empty-state", "Empty state", |_, cx| { - cx.new(|_| EmptyStateDemo).into() - }), - cx, - ); - dock.register_panel( - PanelDescriptor::new("scroll-area", "Scroll area", |_, cx| { - cx.new(|_| ScrollAreaDemo).into() - }), - cx, - ); - dock.register_panel( - PanelDescriptor::new("scrollbar", "Scrollbar", |_, cx| { - cx.new(|_| ScrollbarDemo::new()).into() - }), - cx, - ); - dock.register_panel( - PanelDescriptor::new("code-view", "Code view", |_, cx| { - cx.new(CodeViewDemo::new).into() - }), - cx, - ); - dock.register_panel( - PanelDescriptor::new("diff-view", "Diff view", |_, cx| { - cx.new(DiffViewDemo::new).into() - }), - cx, - ); - dock.register_panel( - PanelDescriptor::new("markdown-view", "Markdown view", |_, cx| { - cx.new(MarkdownViewDemo::new).into() - }), - cx, - ); - dock.register_panel( - PanelDescriptor::new("sidebar", "Sidebar", |_, cx| { - cx.new(|_| SidebarDemo { selected: None }).into() - }), - cx, - ); - dock.register_panel( - PanelDescriptor::new("resize-handle", "Resize handle", |_, cx| { - cx.new(|_| ResizeDemo { width: px(200.) }).into() - }), - cx, - ); - dock.register_panel( - PanelDescriptor::new("resizable", "Resizable", |_, cx| { - cx.new(ResizableDemo::new).into() - }), - cx, - ); - dock.register_panel( - PanelDescriptor::new("separator", "Separator", |_, cx| { - cx.new(|_| SeparatorDemo).into() - }), - cx, - ); - dock.register_panel( - PanelDescriptor::new("theme", "Theme", |_, cx| cx.new(|_| ThemeDemo).into()), - cx, - ); + }, + )); + dock.register_panel(PanelDescriptor::new("popover", "Popover", |_, cx| { + cx.new(|_| PopoverDemo { open: false }).into() + })); + dock.register_panel(PanelDescriptor::new("tooltip", "Tooltip", |_, cx| { + cx.new(|_| TooltipDemo).into() + })); + dock.register_panel(PanelDescriptor::new( + "empty-state", + "Empty state", + |_, cx| cx.new(|_| EmptyStateDemo).into(), + )); + dock.register_panel(PanelDescriptor::new( + "scroll-area", + "Scroll area", + |_, cx| cx.new(|_| ScrollAreaDemo).into(), + )); + dock.register_panel(PanelDescriptor::new("scrollbar", "Scrollbar", |_, cx| { + cx.new(|_| ScrollbarDemo::new()).into() + })); + dock.register_panel(PanelDescriptor::new("code-view", "Code view", |_, cx| { + cx.new(CodeViewDemo::new).into() + })); + dock.register_panel(PanelDescriptor::new("diff-view", "Diff view", |_, cx| { + cx.new(DiffViewDemo::new).into() + })); + dock.register_panel(PanelDescriptor::new( + "markdown-view", + "Markdown view", + |_, cx| cx.new(MarkdownViewDemo::new).into(), + )); + dock.register_panel(PanelDescriptor::new("sidebar", "Sidebar", |_, cx| { + cx.new(|_| SidebarDemo { selected: None }).into() + })); + dock.register_panel(PanelDescriptor::new( + "resize-handle", + "Resize handle", + |_, cx| cx.new(|_| ResizeDemo { width: px(200.) }).into(), + )); + dock.register_panel(PanelDescriptor::new("resizable", "Resizable", |_, cx| { + cx.new(ResizableDemo::new).into() + })); + dock.register_panel(PanelDescriptor::new("separator", "Separator", |_, cx| { + cx.new(|_| SeparatorDemo).into() + })); + dock.register_panel(PanelDescriptor::new("theme", "Theme", |_, cx| { + cx.new(|_| ThemeDemo).into() + })); } }