diff --git a/TASKS.md b/TASKS.md index 50db78b..59c1dd1 100644 --- a/TASKS.md +++ b/TASKS.md @@ -419,3 +419,4 @@ Per-component lines live in `tasks/`. Tags there: | T38a | 1 | FAIL | the resize grip and edge handles hung half outside the box; the edges' sides, a free turn and a band with Shift untested | | T38a | 2 | PASS | | | T38b | 1 | PASS | | +| T39a | 1 | FAIL | a larger text size cut the density strip at 280px; ten palette colors had no name; the density and sliders untested | diff --git a/src/buttons/segmented.rs b/src/buttons/segmented.rs index f34745a..2e59da1 100644 --- a/src/buttons/segmented.rs +++ b/src/buttons/segmented.rs @@ -12,11 +12,12 @@ use crate::{ motion::{self, Axis, Marker, glide, measure_item, measure_origin, slide}, primitives::{FocusRing, Icon, IconName, tab_stop}, theme::{ActiveTheme, ControlSize, Elevation, Radius}, + typography::Ellipsis, }; type OnChange = Rc; -/// Mutually exclusive segments. A thumb slides to the chosen one; too narrow for them, the strip scrolls sideways and a focused segment comes into view. +/// Mutually exclusive segments. A thumb slides to the chosen one. Narrow, the segments give way and their labels end in an ellipsis, down to twice a control's height each; narrower still, the strip scrolls sideways and a focused segment comes into view. #[derive(IntoElement)] pub struct SegmentedControl { id: ElementId, @@ -99,6 +100,7 @@ impl RenderOnce for SegmentedControl { let theme = cx.theme(); let colors = &theme.colors; let (text, icon_size) = label_size(self.size); + let (least, count) = (theme.control_height(self.size) * 2.0, values.len()); let duration = motion::duration(motion::SLOW, cx); let thumb = marker.map( |Marker { @@ -140,6 +142,7 @@ impl RenderOnce for SegmentedControl { .relative() .flex() .flex_1() + .min_w_0() .items_center() .justify_center() .gap_1p5() @@ -172,12 +175,15 @@ impl RenderOnce for SegmentedControl { .when_some(icon, |segment, icon| { segment.child(Icon::new(icon).size(icon_size).color(fg)) }) - .when(!label.is_empty(), |segment| segment.child(label)) + .when(!label.is_empty(), |segment| { + segment.child(div().min_w_0().child(Ellipsis::new(label))) + }) .child(measure_item(measure, ix, Axis::Horizontal)); div() .relative() .flex() .flex_1() + .min_w_0() .child(segment) .child(reveal) }); @@ -186,13 +192,9 @@ impl RenderOnce for SegmentedControl { .relative() .flex() .flex_grow() - .flex_shrink_0() + .flex_shrink() + .min_w(least * count as f32) .items_center() - .p_0p5() - .rounded(theme.radius(Radius::Lg)) - .bg(colors.sunken) - .border_1() - .border_color(colors.border) .child(measure_origin(state.clone(), Axis::Horizontal)) .children(thumb) .children(segments); @@ -201,7 +203,12 @@ impl RenderOnce for SegmentedControl { .id((self.id, "strip")) .flex() .overflow_x_scroll() - .track_scroll(&scroll), + .track_scroll(&scroll) + .p_0p5() + .rounded(theme.radius(Radius::Lg)) + .bg(colors.sunken) + .border_1() + .border_color(colors.border), ) .child(control) } diff --git a/src/settings/tests/theme.rs b/src/settings/tests/theme.rs index 49d6026..4c12ef2 100644 --- a/src/settings/tests/theme.rs +++ b/src/settings/tests/theme.rs @@ -1,9 +1,12 @@ -use gpui::{AnyElement, App, Entity, IntoElement, TestAppContext, Window}; +use gpui::{ + AnyElement, App, Entity, InteractiveElement, IntoElement, ParentElement, Styled, + TestAppContext, Window, +}; -use super::{Desk, desk, said, say, tab, tap}; +use super::{Desk, desk, said, say, settle, tab, tap}; use crate::{ settings::{ThemeDraft, ThemeEditor}, - theme::ActiveTheme, + theme::{ActiveTheme, Theme}, }; fn editor(_: &mut Window, cx: &mut App, owner: Entity) -> AnyElement { @@ -62,3 +65,56 @@ fn a_color_well_sets_its_own_color(cx: &mut TestAppContext) { tap("enter", cx); assert_eq!(said(&host, cx), ["#224466"]); } + +fn look(_: &mut Window, cx: &mut App, owner: Entity) -> AnyElement { + ThemeEditor::new("editor", ThemeDraft::of(cx.theme())) + .on_change(move |draft, _, cx| { + let words = format!( + "{:?} corners {} text {}", + draft.density, draft.radius_scale, draft.font_scale + ); + say(&owner, words, cx) + }) + .into_any_element() +} + +/// Stops: Compact, Standard, Comfortable, corners, text size. Compact picks the density alone, and Right on each slider moves its own measure a step. +#[gpui::test] +fn the_density_and_the_sliders_move_their_own_field(cx: &mut TestAppContext) { + let (host, cx) = desk(look, cx); + tab(1, cx); + tap("space", cx); + tab(4, cx); + tap("right", cx); + tab(5, cx); + tap("right", cx); + assert_eq!( + said(&host, cx), + [ + "Compact corners 1 text 1", + "Standard corners 1.25 text 1", + "Standard corners 1 text 1.05" + ] + ); +} + +fn narrow(window: &mut Window, cx: &mut App, owner: Entity) -> AnyElement { + gpui::div() + .debug_selector(|| "narrow".into()) + .w(gpui::px(280.0)) + .child(editor(window, cx, owner)) + .into_any_element() +} + +/// At 280px with the text a step and a half larger, the density strip keeps every segment inside the box, its labels giving way. +#[gpui::test] +fn the_density_strip_fits_a_narrow_box_with_larger_text(cx: &mut TestAppContext) { + let (_, cx) = desk(narrow, cx); + cx.update(|_, cx| Theme::update(cx, |theme| theme.font_scale = 1.15)); + settle(cx); + let frame = cx.debug_bounds("narrow").expect("the box"); + let last = cx + .debug_bounds("segment Comfortable") + .expect("the last segment"); + assert!(last.right() <= frame.right(), "{last:?} inside {frame:?}"); +} diff --git a/src/theme/palette.rs b/src/theme/palette.rs index 0b61627..226dafd 100644 --- a/src/theme/palette.rs +++ b/src/theme/palette.rs @@ -31,16 +31,33 @@ impl Mix for [Hsla; N] { } macro_rules! mixable { - ($(#[$meta:meta])* $name:ident { $($field:ident: $ty:ty),* $(,)? }) => { + ($(#[$meta:meta])* $name:ident { $($color:ident),* $(,)? ; $($field:ident: $ty:ty),* $(,)? }) => { $(#[$meta])* #[derive(Clone, Debug, PartialEq)] pub struct $name { + $(pub $color: Hsla,)* $(pub $field: $ty,)* } impl Mix for $name { fn mix(&self, to: &Self, t: f32) -> Self { - Self { $($field: self.$field.mix(&to.$field, t),)* } + Self { + $($color: self.$color.mix(&to.$color, t),)* + $($field: self.$field.mix(&to.$field, t),)* + } + } + } + + impl $name { + /// The names of its colors, each a field's. + pub const NAMES: &'static [&'static str] = &[$(stringify!($color)),*]; + + /// The color named `token`, a field's name; any other name fails. + pub fn token_mut(&mut self, token: &str) -> &mut Hsla { + match token { + $(stringify!($color) => &mut self.$color,)* + other => panic!("no {} color {other}", stringify!($name).to_lowercase()), + } } } }; @@ -49,60 +66,62 @@ macro_rules! mixable { mixable!( /// Code token colors. Syntax { - keyword: Hsla, - string: Hsla, - number: Hsla, - comment: Hsla, - function: Hsla, - type_name: Hsla, - constant: Hsla, - property: Hsla, - tag: Hsla, - attribute: Hsla, - operator: Hsla, - punctuation: Hsla, - variable: Hsla, + keyword, + string, + number, + comment, + function, + type_name, + constant, + property, + tag, + attribute, + operator, + punctuation, + variable, + ; } ); mixable!( /// Semantic colors. Components read these, never literals. Palette { - bg: Hsla, - surface: Hsla, - sunken: Hsla, - overlay: Hsla, - hover: Hsla, - active: Hsla, - border: Hsla, - border_strong: Hsla, - fg: Hsla, - fg_muted: Hsla, - fg_subtle: Hsla, - fg_disabled: Hsla, - accent: Hsla, - accent_hover: Hsla, - on_accent: Hsla, - on_media: Hsla, - focus: Hsla, - link: Hsla, - selection: Hsla, - success: Hsla, - warning: Hsla, - danger: Hsla, - info: Hsla, - success_subtle: Hsla, - warning_subtle: Hsla, - danger_subtle: Hsla, - info_subtle: Hsla, - backdrop: Hsla, - media_backdrop: Hsla, - shimmer: Hsla, - shadow: Hsla, - tooltip_bg: Hsla, - tooltip_fg: Hsla, - paper: Hsla, - ink: Hsla, + bg, + surface, + sunken, + overlay, + hover, + active, + border, + border_strong, + fg, + fg_muted, + fg_subtle, + fg_disabled, + accent, + accent_hover, + on_accent, + on_media, + focus, + link, + selection, + success, + warning, + danger, + info, + success_subtle, + warning_subtle, + danger_subtle, + info_subtle, + backdrop, + media_backdrop, + shimmer, + shadow, + tooltip_bg, + tooltip_fg, + paper, + ink, + ; chart: [Hsla; 8], ansi: [Hsla; 16], syntax: Syntax, @@ -122,61 +141,7 @@ pub const HUE_NAMES: [&str; 8] = [ "Blue", "Teal", "Ochre", "Rose", "Violet", "Green", "Rust", "Cyan", ]; -impl Syntax { - /// The color of the token kind named `token`, a field's name; any other name fails. - pub fn token_mut(&mut self, token: &str) -> &mut Hsla { - match token { - "keyword" => &mut self.keyword, - "string" => &mut self.string, - "number" => &mut self.number, - "comment" => &mut self.comment, - "function" => &mut self.function, - "type_name" => &mut self.type_name, - "constant" => &mut self.constant, - "property" => &mut self.property, - "tag" => &mut self.tag, - "attribute" => &mut self.attribute, - "operator" => &mut self.operator, - "punctuation" => &mut self.punctuation, - "variable" => &mut self.variable, - other => panic!("no syntax color {other}"), - } - } -} - impl Palette { - /// The color named `token`, a field's name; any other name fails. - pub fn token_mut(&mut self, token: &str) -> &mut Hsla { - match token { - "bg" => &mut self.bg, - "surface" => &mut self.surface, - "sunken" => &mut self.sunken, - "overlay" => &mut self.overlay, - "hover" => &mut self.hover, - "active" => &mut self.active, - "border" => &mut self.border, - "border_strong" => &mut self.border_strong, - "fg" => &mut self.fg, - "fg_muted" => &mut self.fg_muted, - "fg_subtle" => &mut self.fg_subtle, - "fg_disabled" => &mut self.fg_disabled, - "accent" => &mut self.accent, - "accent_hover" => &mut self.accent_hover, - "on_accent" => &mut self.on_accent, - "focus" => &mut self.focus, - "link" => &mut self.link, - "selection" => &mut self.selection, - "success" => &mut self.success, - "warning" => &mut self.warning, - "danger" => &mut self.danger, - "info" => &mut self.info, - "tooltip_bg" => &mut self.tooltip_bg, - "tooltip_fg" => &mut self.tooltip_fg, - "shadow" => &mut self.shadow, - other => panic!("no palette color {other}"), - } - } - /// The chart color at `hue`; a hue past the chart fails, naming `owner`. pub fn hue(&self, hue: usize, owner: impl std::fmt::Display) -> Hsla { *self diff --git a/src/theme/tests.rs b/src/theme/tests.rs index 499feeb..a60b05b 100644 --- a/src/theme/tests.rs +++ b/src/theme/tests.rs @@ -1,6 +1,6 @@ use gpui::{TestAppContext, hsla}; -use super::{ActiveTheme, Mode, Palette, Theme}; +use super::{ActiveTheme, Mode, Palette, Syntax, Theme}; /// An owner's palette serves its own mode; the other keeps Ely's, and none brings Ely's back. #[gpui::test] @@ -19,12 +19,29 @@ fn an_owners_palette_serves_its_mode_alone(cx: &mut TestAppContext) { assert_eq!(cx.read(|cx| cx.theme().palette()), Palette::light(false)); } +/// Every color of the palette and of code answers to its field's name, the quiet fills too. #[test] -fn every_named_color_answers() { +fn every_color_answers_to_its_name() { let mut palette = Palette::light(false); - *palette.token_mut("accent") = hsla(0.1, 0.2, 0.3, 1.0); - assert_eq!(palette.accent, hsla(0.1, 0.2, 0.3, 1.0)); - *palette.syntax.token_mut("comment") = hsla(0.4, 0.2, 0.3, 1.0); + for (ix, name) in Palette::NAMES.iter().enumerate() { + *palette.token_mut(name) = hsla(ix as f32 / 64.0, 0.5, 0.5, 1.0); + } + assert_eq!(Palette::NAMES.len(), 35, "every color of the palette"); + let at = |name: &str| { + Palette::NAMES + .iter() + .position(|each| *each == name) + .expect("a name") + }; + assert_eq!( + palette.success_subtle, + hsla(at("success_subtle") as f32 / 64.0, 0.5, 0.5, 1.0) + ); + assert_eq!(palette.ink, hsla(at("ink") as f32 / 64.0, 0.5, 0.5, 1.0)); + for name in Syntax::NAMES { + *palette.syntax.token_mut(name) = hsla(0.4, 0.2, 0.3, 1.0); + } + assert_eq!(Syntax::NAMES.len(), 13, "every color of code"); assert_eq!(palette.syntax.comment, hsla(0.4, 0.2, 0.3, 1.0)); }