diff --git a/AGENTS.md b/AGENTS.md index b076533..388c765 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -168,7 +168,7 @@ Ely GPUI Component. A component library for GPUI, in light and dark. - A box that shows a host's picture takes the picture's shape from the host (`ratio`), so nothing is cropped and a point given as a share lands on the picture. A focusable one keeps its focus border on an outer box, so the picture keeps that shape exactly (`media::ImageCropper`). - A default only the component can work out, such as a crop fitted to a picture it decodes, reaches the owner once through `window.defer` after render (`media::ImageUpload`). - A ScrubInput shows its number in about 140px beside a short label. In a narrower column a pair hides it, so a long label takes a row of its own (`canvas::TypographyPanel`). -- Design panels edit a whole value and hand the owner a changed copy through `canvas::panel::editing`. A swatch that opens a picker is `canvas::panel::color_well`. +- Design panels edit a whole value and hand the owner a changed copy through `canvas::panel::editing`, which takes the owner's handler itself: a component without one fails at render. A swatch that opens a picker is `canvas::panel::color_well`. - A press that stops propagation also stops the ancestors' focus on press. A layer that takes keys focuses itself in its own press handler (`canvas::ToolLayer`, `canvas::MindMap`), and a field over it stops its own presses. - A button inside a pressable row goes through `lists::row_action`, which keeps its press and its Space or Enter from the row (`devtools::ConnectionManager`). - A field opened over a component hands focus back to that component on Enter and Escape, through `forms::Editing`'s `returning` and `give_back` (`canvas::writing`); a blur keeps the words and leaves focus where it went. diff --git a/TASKS.md b/TASKS.md index 7e3e8d7..0cfd408 100644 --- a/TASKS.md +++ b/TASKS.md @@ -398,3 +398,4 @@ Per-component lines live in `tasks/`. Tags there: | T34 | 1 | FAIL | a focused tile's Left and Right, and the guard that leaves arrows to a focused control inside a card, untested | | T34 | 2 | PASS | | | T35 | 1 | FAIL | a row's control kept its full width past a 280px box; a notice's name shrank to nothing; privacy, notices and startup dropped edits with no handler; Apply's and the reset's rest and a box turned off untested | +| T35 | 2 | FAIL | an accent picker of nine or more presets still ran past a 280px box | diff --git a/src/settings/appearance.rs b/src/settings/appearance.rs index 389245c..0a6f398 100644 --- a/src/settings/appearance.rs +++ b/src/settings/appearance.rs @@ -1,8 +1,8 @@ use std::rc::Rc; use gpui::{ - App, ElementId, Hsla, IntoElement, ParentElement, Pixels, RenderOnce, SharedString, Styled, - Window, div, + App, ElementId, Hsla, InteractiveElement, IntoElement, ParentElement, Pixels, RenderOnce, + SharedString, Styled, Window, div, }; use crate::{ @@ -140,14 +140,17 @@ impl RenderOnce for AccentColorPicker { }; let columns = u16::try_from(self.presets.len()).expect("a handful of presets"); div() + .debug_selector(|| "accent-color-picker".into()) .flex() .items_start() .gap_2() .child( - ColorPalette::new((self.id.clone(), "presets"), self.presets) - .columns(columns) - .selected(self.color) - .on_change(report(picked)), + div().flex_1().min_w_0().child( + ColorPalette::new((self.id.clone(), "presets"), self.presets) + .columns(columns) + .selected(self.color) + .on_change(report(picked)), + ), ) .child(color_well( (self.id, "custom").into(), diff --git a/src/settings/tests/appearance.rs b/src/settings/tests/appearance.rs new file mode 100644 index 0000000..f8998a8 --- /dev/null +++ b/src/settings/tests/appearance.rs @@ -0,0 +1,63 @@ +use gpui::{ + AnyElement, App, Entity, IntoElement, ParentElement, Styled, TestAppContext, Window, div, hsla, + px, +}; + +use super::{Desk, desk, said, say, tab, tap}; +use crate::settings::{AccentColorPicker, FontSizeControl}; + +fn accent(_: &mut Window, _: &mut App, owner: Entity) -> AnyElement { + let (red, blue) = (hsla(0.0, 0.6, 0.5, 1.0), hsla(0.6, 0.6, 0.5, 1.0)); + AccentColorPicker::new("accent", red, [("Red", red), ("Blue", blue)]) + .on_change(move |color, _, cx| say(&owner, format!("blue {}", color == blue), cx)) + .into_any_element() +} + +/// Stops: the presets, Red then Blue, then the custom well. +#[gpui::test] +fn a_preset_sets_the_accent(cx: &mut TestAppContext) { + let (host, cx) = desk(accent, cx); + tab(2, cx); + tap("space", cx); + assert_eq!(said(&host, cx), ["blue true"]); +} + +fn default_size(_: &mut Window, _: &mut App, owner: Entity) -> AnyElement { + FontSizeControl::new("size", 14.0, 14.0, (12.0, 24.0)) + .on_change(move |size, _, cx| say(&owner, format!("size {size}"), cx)) + .into_any_element() +} + +/// At the default size the way back rests, so the second stop wraps to the slider. +#[gpui::test] +fn the_way_back_rests_at_the_default_size(cx: &mut TestAppContext) { + let (host, cx) = desk(default_size, cx); + tab(2, cx); + tap("space", cx); + assert!(said(&host, cx).is_empty()); +} + +fn many_presets(_: &mut Window, _: &mut App, _: Entity) -> AnyElement { + let presets: Vec<(String, gpui::Hsla)> = (0..12) + .map(|ix| (format!("hue {ix}"), hsla(ix as f32 / 12.0, 0.5, 0.5, 1.0))) + .collect(); + div() + .w(px(280.0)) + .child(AccentColorPicker::new("accent", presets[0].1, presets).on_change(|_, _, _| {})) + .into_any_element() +} + +/// Twelve swatches do not fit in one 280px row, so they fold under each other inside the box. +#[gpui::test] +fn many_presets_fold_inside_a_narrow_box(cx: &mut TestAppContext) { + let (_, cx) = desk(many_presets, cx); + let picker = cx.debug_bounds("accent-color-picker").expect("the picker"); + assert!( + picker.size.width <= px(280.0), + "no wider than its box: {picker:?}" + ); + assert!( + picker.size.height > px(40.0), + "its swatches fold to a second row: {picker:?}" + ); +} diff --git a/src/settings/tests.rs b/src/settings/tests/mod.rs similarity index 89% rename from src/settings/tests.rs rename to src/settings/tests/mod.rs index 49d8ef4..a74cc8b 100644 --- a/src/settings/tests.rs +++ b/src/settings/tests/mod.rs @@ -1,13 +1,13 @@ use gpui::{ AnyElement, App, Context, Entity, IntoElement, KeyBinding, ParentElement, Render, Styled, - TestAppContext, VisualTestContext, Window, div, hsla, px, + TestAppContext, VisualTestContext, Window, div, px, }; use super::{ - AccentColorPicker, Appearance, Changed, DeveloperModeToggle, FeatureFlags, Flag, - FontSizeControl, ImportExportSettings, Notices, NotificationSettings, Privacy, PrivacySettings, - Proxy, ProxySettings, ResetToDefault, SettingEntry, SettingsLayout, SettingsSearch, Stage, - Startup, StartupSettings, StorageSettings, Store, ThemeSelector, + Appearance, Changed, DeveloperModeToggle, FeatureFlags, Flag, FontSizeControl, + ImportExportSettings, Notices, NotificationSettings, Privacy, PrivacySettings, Proxy, + ProxySettings, ResetToDefault, SettingEntry, SettingsLayout, SettingsSearch, Stage, Startup, + StartupSettings, StorageSettings, Store, ThemeSelector, }; use crate::{ forms::{self, Choice, TextInput}, @@ -306,22 +306,6 @@ fn the_arrows_open_the_next_section(cx: &mut TestAppContext) { assert_eq!(said(&host, cx), ["open b"]); } -fn accent(_: &mut Window, _: &mut App, owner: Entity) -> AnyElement { - let (red, blue) = (hsla(0.0, 0.6, 0.5, 1.0), hsla(0.6, 0.6, 0.5, 1.0)); - AccentColorPicker::new("accent", red, [("Red", red), ("Blue", blue)]) - .on_change(move |color, _, cx| say(&owner, format!("blue {}", color == blue), cx)) - .into_any_element() -} - -/// Stops: the presets, Red then Blue, then the custom well. -#[gpui::test] -fn a_preset_sets_the_accent(cx: &mut TestAppContext) { - let (host, cx) = desk(accent, cx); - tab(2, cx); - tap("space", cx); - assert_eq!(said(&host, cx), ["blue true"]); -} - fn privacy(_: &mut Window, _: &mut App, owner: Entity) -> AnyElement { let privacy = Privacy { telemetry: true, @@ -455,21 +439,6 @@ fn apply_rests_while_the_port_is_bad(cx: &mut TestAppContext) { assert!(said(&host, cx).is_empty()); } -fn default_size(_: &mut Window, _: &mut App, owner: Entity) -> AnyElement { - FontSizeControl::new("size", 14.0, 14.0, (12.0, 24.0)) - .on_change(move |size, _, cx| say(&owner, format!("size {size}"), cx)) - .into_any_element() -} - -/// At the default size the way back rests, so the second stop wraps to the slider. -#[gpui::test] -fn the_way_back_rests_at_the_default_size(cx: &mut TestAppContext) { - let (host, cx) = desk(default_size, cx); - tab(2, cx); - tap("space", cx); - assert!(said(&host, cx).is_empty()); -} - /// Stops: Do not disturb, then App, which is on. #[gpui::test] fn a_box_pressed_off_lets_its_pair_go(cx: &mut TestAppContext) { @@ -478,3 +447,5 @@ fn a_box_pressed_off_lets_its_pair_go(cx: &mut TestAppContext) { tap("space", cx); assert_eq!(said(&host, cx), ["[] false"]); } + +mod appearance;