audit select

This commit is contained in:
2026-11-07 19:30:04 +07:00
parent ffbb732f5f
commit a859704185
8 changed files with 262 additions and 138 deletions
+31 -39
View File
@@ -127,6 +127,7 @@ impl MenuItem {
struct MenuChrome { struct MenuChrome {
surface: gpui::Hsla, surface: gpui::Hsla,
border: gpui::Hsla, border: gpui::Hsla,
panel_border: gpui::Hsla,
foreground: gpui::Hsla, foreground: gpui::Hsla,
muted_foreground: gpui::Hsla, muted_foreground: gpui::Hsla,
primary: gpui::Hsla, primary: gpui::Hsla,
@@ -242,6 +243,11 @@ impl RenderOnce for Menu {
MenuChrome { MenuChrome {
surface: theme.colors.surface, surface: theme.colors.surface,
border: theme.colors.border, border: theme.colors.border,
panel_border: if theme.borders.overlays {
theme.colors.border
} else {
theme.colors.surface
},
foreground: theme.colors.foreground, foreground: theme.colors.foreground,
muted_foreground: theme.colors.muted_foreground, muted_foreground: theme.colors.muted_foreground,
primary: theme.ramps.primary.base, primary: theme.ramps.primary.base,
@@ -259,7 +265,6 @@ impl RenderOnce for Menu {
}; };
let trigger_bounds: Rc<RefCell<Option<Bounds<Pixels>>>> = Rc::new(RefCell::new(None)); let trigger_bounds: Rc<RefCell<Option<Bounds<Pixels>>>> = Rc::new(RefCell::new(None));
let panel_bounds: Rc<RefCell<Option<Bounds<Pixels>>>> = Rc::new(RefCell::new(None));
let selection = window.use_keyed_state("menu-selection", cx, |_, _| 0usize); let selection = window.use_keyed_state("menu-selection", cx, |_, _| 0usize);
let was_open = window.use_keyed_state("menu-was-open", cx, |_, _| self.open); let was_open = window.use_keyed_state("menu-was-open", cx, |_, _| self.open);
@@ -363,6 +368,7 @@ impl RenderOnce for Menu {
let panel = v_flex() let panel = v_flex()
.id("menu-panel") .id("menu-panel")
.occlude()
.track_focus(&focus) .track_focus(&focus)
.role(Role::Menu) .role(Role::Menu)
.aria_label(label) .aria_label(label)
@@ -373,9 +379,25 @@ impl RenderOnce for Menu {
.text_color(chrome.foreground) .text_color(chrome.foreground)
.rounded(chrome.radius_overlay) .rounded(chrome.radius_overlay)
.border_1() .border_1()
.border_color(chrome.border) .border_color(chrome.panel_border)
.shadow(chrome.shadow.clone()) .shadow(chrome.shadow.clone())
.overflow_hidden() .overflow_hidden()
.on_mouse_down_out({
let on_change = on_change.clone();
let trigger_bounds = trigger_bounds.clone();
move |event, window, cx| {
if trigger_bounds
.borrow()
.is_some_and(|bounds| bounds.contains(&event.position))
{
return;
}
if let Some(on_change) = &on_change {
on_change(false, window, cx);
}
}
})
.on_key_down({ .on_key_down({
let items = items.clone(); let items = items.clone();
let on_change = on_change.clone(); let on_change = on_change.clone();
@@ -392,44 +414,14 @@ impl RenderOnce for Menu {
.children(rows), .children(rows),
) )
.child( .child(
canvas( canvas(|_, _, _| {}, {
{ let focus = focus.clone();
let cell = panel_bounds.clone(); move |_, _, window, cx| {
move |bounds, _window, _cx| *cell.borrow_mut() = Some(bounds) if !focus.contains_focused(window, cx) {
}, window.focus(&focus, cx);
{
let on_change = on_change.clone();
move |_, _, window, cx| {
if !focus.contains_focused(window, cx) {
window.focus(&focus, cx);
}
window.on_mouse_event({
let on_change = on_change.clone();
move |event: &MouseDownEvent, phase, window, cx| {
if phase != DispatchPhase::Bubble {
return;
}
let on_panel = panel_bounds
.borrow()
.is_some_and(|bounds| bounds.contains(&event.position));
let on_trigger = trigger_bounds
.borrow()
.is_some_and(|bounds| bounds.contains(&event.position));
if on_panel || on_trigger {
return;
}
if let Some(on_change) = &on_change {
on_change(false, window, cx);
}
cx.stop_propagation();
}
});
} }
}, }
) })
.absolute() .absolute()
.inset_0(), .inset_0(),
); );
+23 -43
View File
@@ -102,7 +102,6 @@ impl RenderOnce for Popover {
}; };
let trigger_bounds: BoundsCell = Rc::new(RefCell::new(None)); let trigger_bounds: BoundsCell = Rc::new(RefCell::new(None));
let panel_bounds: BoundsCell = Rc::new(RefCell::new(None));
let fallback_label = self let fallback_label = self
.a11y_label .a11y_label
@@ -166,6 +165,7 @@ impl RenderOnce for Popover {
let panel = v_flex() let panel = v_flex()
.id("popover-panel") .id("popover-panel")
.occlude()
.track_focus(&focus) .track_focus(&focus)
.role(Role::Dialog) .role(Role::Dialog)
.aria_label(label) .aria_label(label)
@@ -178,6 +178,21 @@ impl RenderOnce for Popover {
.border_color(colors.border) .border_color(colors.border)
.shadow(shadows) .shadow(shadows)
.overflow_hidden() .overflow_hidden()
.on_mouse_down_out({
let on_change = on_change.clone();
let trigger_bounds = trigger_bounds.clone();
move |event, window, cx| {
if trigger_bounds
.borrow()
.is_some_and(|bounds| bounds.contains(&event.position))
{
return;
}
if let Some(on_change) = &on_change {
on_change(false, window, cx);
}
}
})
.on_key_down({ .on_key_down({
let on_change = on_change.clone(); let on_change = on_change.clone();
move |event: &KeyDownEvent, window, cx| { move |event: &KeyDownEvent, window, cx| {
@@ -191,49 +206,14 @@ impl RenderOnce for Popover {
}) })
.when_some(content, |panel, content| panel.child(content)) .when_some(content, |panel, content| panel.child(content))
.child( .child(
canvas( canvas(|_, _, _| {}, {
{ let focus = focus;
let cell = panel_bounds.clone(); move |_, _, window, cx| {
move |bounds, _window, _cx| *cell.borrow_mut() = Some(bounds) if !focus.contains_focused(window, cx) {
}, window.focus(&focus, cx);
{
let on_change = on_change.clone();
let focus = focus;
move |_, _, window, cx| {
if !focus.contains_focused(window, cx) {
window.focus(&focus, cx);
}
window.on_mouse_event({
let on_change = on_change.clone();
move |event: &MouseDownEvent, phase, window, cx| {
if phase != DispatchPhase::Bubble
|| event.button != MouseButton::Left
{
return;
}
let on_panel = panel_bounds
.borrow()
.is_some_and(|bounds| bounds.contains(&event.position));
let on_trigger = trigger_bounds
.borrow()
.is_some_and(|bounds| bounds.contains(&event.position));
if on_panel || on_trigger {
return;
}
if let Some(on_change) = &on_change {
on_change(false, window, cx);
}
cx.stop_propagation();
}
});
} }
}, }
) })
.absolute() .absolute()
.inset_0(), .inset_0(),
); );
+27 -5
View File
@@ -6,7 +6,8 @@ use gpui::{
SharedString, StatefulInteractiveElement as _, StyleRefinement, Styled as _, Window, SharedString, StatefulInteractiveElement as _, StyleRefinement, Styled as _, Window,
}; };
use gpui_native_icons::{IconRole, icon}; use gpui_native_icons::{IconRole, icon};
use gpui_native_theme::{ActiveTheme as _, Size}; use gpui_native_theme::{ActiveTheme as _, BORDER_WIDTH, Size};
use gpui_native_utils::focus_ring;
use crate::common::{Sizable, StyledExt, h_flex}; use crate::common::{Sizable, StyledExt, h_flex};
use crate::menu::{Menu, MenuItem}; use crate::menu::{Menu, MenuItem};
@@ -169,6 +170,8 @@ where
let muted_foreground = theme.colors.muted_foreground; let muted_foreground = theme.colors.muted_foreground;
let radius = theme.radius.control; let radius = theme.radius.control;
let icon_size = metrics.dense_icon_size; let icon_size = metrics.dense_icon_size;
let fields_bordered = theme.borders.fields;
let focus_ring_on = theme.borders.focus_ring;
let label = self let label = self
.a11y_label .a11y_label
@@ -180,7 +183,22 @@ where
.use_keyed_state("select-open", cx, |_, _| false) .use_keyed_state("select-open", cx, |_, _| false)
.clone() .clone()
}); });
let is_open = *open.read(cx); let is_open = *open.read(cx);
let open_for_items = open.clone();
let (rest_border, hover_border) = if fields_bordered {
(
if is_open && !focus_ring_on {
ring
} else {
border
},
border,
)
} else {
(input, muted)
};
let selected_index = self.selected.as_ref().and_then(|value| { let selected_index = self.selected.as_ref().and_then(|value| {
self.options self.options
@@ -194,6 +212,7 @@ where
let trigger_id = self.id.clone(); let trigger_id = self.id.clone();
let trigger_label = label.clone(); let trigger_label = label.clone();
let trigger_style = self.style; let trigger_style = self.style;
let trigger = move |_window: &mut Window, _cx: &mut App| { let trigger = move |_window: &mut Window, _cx: &mut App| {
let text = trigger_selected let text = trigger_selected
.as_ref() .as_ref()
@@ -207,17 +226,18 @@ where
h_flex() h_flex()
.id((trigger_id.clone(), "select-trigger")) .id((trigger_id.clone(), "select-trigger"))
.debug_selector(|| "select-trigger".to_string())
.role(Role::ComboBox) .role(Role::ComboBox)
.aria_label(trigger_label.clone()) .aria_label(trigger_label.clone())
.cursor_pointer() .cursor_pointer()
.justify_between() .justify_between()
.gap_2() .gap_2()
.w_full() .w_full()
.h(metrics.input_height) .h(metrics.input_height + BORDER_WIDTH * 2.0)
.px(metrics.dense_padding_x) .px(metrics.dense_padding_x)
.rounded(radius) .rounded(radius)
.border_1() .border_1()
.border_color(if is_open { ring } else { border }) .border_color(rest_border)
.bg(input) .bg(input)
.text_size(metrics.text_size) .text_size(metrics.text_size)
.text_color(if trigger_selected.is_some() { .text_color(if trigger_selected.is_some() {
@@ -225,7 +245,10 @@ where
} else { } else {
muted_foreground muted_foreground
}) })
.hover(|style| style.bg(muted)) .when(is_open && focus_ring_on, |trigger| {
trigger.shadow(focus_ring(ring))
})
.hover(|style| style.bg(muted).border_color(hover_border))
.child(text) .child(text)
.child( .child(
icon(IconRole::ChevronDown) icon(IconRole::ChevronDown)
@@ -236,7 +259,6 @@ where
.into_any_element() .into_any_element()
}; };
let open_for_items = open.clone();
let items = move |_window: &mut Window, _cx: &mut App| { let items = move |_window: &mut Window, _cx: &mut App| {
self.options self.options
.iter() .iter()
+68 -35
View File
@@ -1,8 +1,9 @@
use std::{cell::RefCell, rc::Rc}; use std::{cell::RefCell, rc::Rc};
use gpui::{ use gpui::{
Context, IntoElement, Modifiers, MouseButton, ParentElement as _, Render, Styled as _, Context, InteractiveElement as _, IntoElement, Modifiers, MouseButton, ParentElement as _,
TestAppContext, VisualTestContext, Window, WindowHandle, div, point, px, rgba, Render, StatefulInteractiveElement as _, Styled as _, TestAppContext, VisualTestContext,
Window, WindowHandle, div, point, px, rgba,
}; };
use gpui_native_core::popover::Popover; use gpui_native_core::popover::Popover;
use gpui_native_theme::Theme; use gpui_native_theme::Theme;
@@ -10,37 +11,53 @@ use gpui_native_theme::Theme;
struct Harness { struct Harness {
open: bool, open: bool,
changes: Rc<RefCell<Vec<bool>>>, changes: Rc<RefCell<Vec<bool>>>,
target_clicks: Rc<RefCell<usize>>,
} }
impl Render for Harness { impl Render for Harness {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let changes = self.changes.clone(); let changes = self.changes.clone();
let target_clicks = self.target_clicks.clone();
let view = cx.entity(); let view = cx.entity();
div().size_full().child( div()
Popover::new("test-popover") .size_full()
.open(self.open) .child(
.on_change(move |next, _, cx| { div().absolute().left(px(400.)).top(px(300.)).child(
changes.borrow_mut().push(next);
view.update(cx, |harness, cx| {
harness.open = next;
cx.notify();
});
})
.trigger(|_, _| {
div() div()
.w(px(100.)) .id("popover-target")
.h(px(32.)) .size(px(40.))
.bg(rgba(0x000000ff)) .bg(rgba(0x000000ff))
.into_any_element() .on_click(move |_, _, cx| {
}) *target_clicks.borrow_mut() += 1;
.content(|_, _| { cx.stop_propagation();
div() }),
.w(px(200.)) ),
.h(px(80.)) )
.child("Popover content") .child(
.into_any_element() Popover::new("test-popover")
}), .open(self.open)
) .on_change(move |next, _, cx| {
changes.borrow_mut().push(next);
view.update(cx, |harness, cx| {
harness.open = next;
cx.notify();
});
})
.trigger(|_, _| {
div()
.w(px(100.))
.h(px(32.))
.bg(rgba(0x000000ff))
.into_any_element()
})
.content(|_, _| {
div()
.w(px(200.))
.h(px(80.))
.child("Popover content")
.into_any_element()
}),
)
} }
} }
@@ -48,27 +65,31 @@ impl Render for Harness {
/// hangs below it around `(0..200, 36..116)`, so clicks at `(10, 16)` land /// hangs below it around `(0..200, 36..116)`, so clicks at `(10, 16)` land
/// on the trigger, `(100, 70)` on the panel, and anything past `(300, 300)` /// on the trigger, `(100, 70)` on the panel, and anything past `(300, 300)`
/// is outside both. /// is outside both.
fn popover_window( type PopoverWindow = (
open: bool,
) -> (
TestAppContext, TestAppContext,
VisualTestContext, VisualTestContext,
WindowHandle<Harness>, WindowHandle<Harness>,
Rc<RefCell<Vec<bool>>>, Rc<RefCell<Vec<bool>>>,
) { Rc<RefCell<usize>>,
);
fn popover_window(open: bool) -> PopoverWindow {
let mut cx = TestAppContext::single(); let mut cx = TestAppContext::single();
cx.update(Theme::init); cx.update(Theme::init);
let changes = Rc::new(RefCell::new(Vec::new())); let changes = Rc::new(RefCell::new(Vec::new()));
let harness_changes = changes.clone(); let harness_changes = changes.clone();
let target_clicks = Rc::new(RefCell::new(0));
let harness_clicks = target_clicks.clone();
let window = cx.add_window(move |_, _| Harness { let window = cx.add_window(move |_, _| Harness {
open, open,
changes: harness_changes, changes: harness_changes,
target_clicks: harness_clicks,
}); });
let mut visual = VisualTestContext::from_window(window.into(), &cx); let mut visual = VisualTestContext::from_window(window.into(), &cx);
visual.update(|window, cx| { visual.update(|window, cx| {
window.draw(cx).clear(cx); window.draw(cx).clear(cx);
}); });
(cx, visual, window, changes) (cx, visual, window, changes, target_clicks)
} }
fn draw(visual: &mut VisualTestContext) { fn draw(visual: &mut VisualTestContext) {
@@ -84,7 +105,7 @@ fn click(visual: &mut VisualTestContext, position: gpui::Point<gpui::Pixels>) {
#[test] #[test]
fn clicking_outside_dismisses_and_reports_closed() { fn clicking_outside_dismisses_and_reports_closed() {
let (_cx, mut visual, _window, changes) = popover_window(true); let (_cx, mut visual, _window, changes, _clicks) = popover_window(true);
click(&mut visual, point(px(600.), px(600.))); click(&mut visual, point(px(600.), px(600.)));
assert_eq!(*changes.borrow(), vec![false]); assert_eq!(*changes.borrow(), vec![false]);
@@ -98,7 +119,7 @@ fn clicking_outside_dismisses_and_reports_closed() {
#[test] #[test]
fn clicking_the_trigger_toggles_and_clicking_the_panel_does_nothing() { fn clicking_the_trigger_toggles_and_clicking_the_panel_does_nothing() {
let (_cx, mut visual, _window, changes) = popover_window(true); let (_cx, mut visual, _window, changes, _clicks) = popover_window(true);
// On the open panel's trigger: toggles closed. // On the open panel's trigger: toggles closed.
click(&mut visual, point(px(10.), px(16.))); click(&mut visual, point(px(10.), px(16.)));
@@ -111,7 +132,7 @@ fn clicking_the_trigger_toggles_and_clicking_the_panel_does_nothing() {
#[test] #[test]
fn clicking_the_trigger_opens_a_closed_popover() { fn clicking_the_trigger_opens_a_closed_popover() {
let (_cx, mut visual, _window, changes) = popover_window(false); let (_cx, mut visual, _window, changes, _clicks) = popover_window(false);
click(&mut visual, point(px(10.), px(16.))); click(&mut visual, point(px(10.), px(16.)));
assert_eq!(*changes.borrow(), vec![true]); assert_eq!(*changes.borrow(), vec![true]);
@@ -119,7 +140,7 @@ fn clicking_the_trigger_opens_a_closed_popover() {
#[test] #[test]
fn escape_dismisses_while_the_panel_holds_focus() { fn escape_dismisses_while_the_panel_holds_focus() {
let (_cx, mut visual, _window, changes) = popover_window(true); let (_cx, mut visual, _window, changes, _clicks) = popover_window(true);
draw(&mut visual); draw(&mut visual);
// The panel claims focus on open, so escape reaches its key listener. // The panel claims focus on open, so escape reaches its key listener.
@@ -134,7 +155,7 @@ fn escape_dismisses_while_the_panel_holds_focus() {
#[test] #[test]
fn the_controlled_loop_reopens_from_the_app_side() { fn the_controlled_loop_reopens_from_the_app_side() {
let (mut cx, mut visual, window, changes) = popover_window(true); let (mut cx, mut visual, window, changes, _clicks) = popover_window(true);
// Dismiss once. // Dismiss once.
click(&mut visual, point(px(600.), px(600.))); click(&mut visual, point(px(600.), px(600.)));
@@ -155,3 +176,15 @@ fn the_controlled_loop_reopens_from_the_app_side() {
visual.simulate_keystrokes("escape"); visual.simulate_keystrokes("escape");
assert_eq!(*changes.borrow(), vec![false, false]); assert_eq!(*changes.borrow(), vec![false, false]);
} }
/// Clicking another control while the popover is open dismisses it and still
/// acts on that control: dismissing does not claim the event.
#[test]
fn clicking_another_control_dismisses_and_reaches_it() {
let (_cx, mut visual, _window, changes, clicks) = popover_window(true);
click(&mut visual, point(px(420.), px(320.)));
assert_eq!(*changes.borrow(), vec![false]);
assert_eq!(*clicks.borrow(), 1);
}
+81 -3
View File
@@ -1,11 +1,12 @@
use std::{cell::RefCell, rc::Rc}; use std::{cell::RefCell, rc::Rc};
use gpui::{ use gpui::{
Context, IntoElement, MouseButton, ParentElement as _, Render, Styled as _, TestAppContext, Context, Entity, IntoElement, MouseButton, ParentElement as _, Render, Styled as _,
VisualTestContext, Window, div, point, px, TestAppContext, VisualTestContext, Window, div, point, px,
}; };
use gpui_native_core::input::{Input, InputState};
use gpui_native_core::select::Select; use gpui_native_core::select::Select;
use gpui_native_theme::Theme; use gpui_native_theme::{ActiveTheme as _, BORDER_WIDTH, Theme};
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Strategy { enum Strategy {
@@ -200,3 +201,80 @@ fn two_selects_keep_their_popups_independent() {
assert_eq!(*first_changes.borrow(), vec![]); assert_eq!(*first_changes.borrow(), vec![]);
assert_eq!(*second_changes.borrow(), vec![Some(Strategy::Curated)]); assert_eq!(*second_changes.borrow(), vec![Some(Strategy::Curated)]);
} }
#[test]
fn a_click_outside_closes_the_popup() {
let (_cx, mut visual, changes) = select_window();
click(&mut visual, point(px(100.), px(31.)));
draw(&mut visual);
click(&mut visual, point(px(100.), px(300.)));
draw(&mut visual);
// The popup held keyboard focus while open; a keystroke after the outside
// click answers nothing if the popup really closed.
visual.simulate_keystrokes("enter");
draw(&mut visual);
assert_eq!(*changes.borrow(), Vec::<Option<Strategy>>::new());
}
/// Clicking another control while the popup is open dismisses it and still
/// acts on the control: dismissing does not claim the event.
#[test]
fn a_click_on_another_control_dismisses_and_reaches_it() {
let (_cx, mut visual, first_changes, second_changes) = pair_window();
click(&mut visual, point(px(100.), px(31.)));
draw(&mut visual);
// The second select's trigger, below the first — at x=300, past the first
// popup's 182px-wide panel: this click must close the first popup AND open
// the second.
click(&mut visual, point(px(300.), px(69.)));
draw(&mut visual);
visual.simulate_keystrokes("enter");
draw(&mut visual);
assert_eq!(*first_changes.borrow(), Vec::<Option<Strategy>>::new());
assert_eq!(*second_changes.borrow(), vec![Some(Strategy::Curated)]);
}
struct HeightsHarness {
state: Entity<InputState>,
}
impl Render for HeightsHarness {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div()
.flex()
.flex_col()
.gap_4()
.child(Select::new("heights").options(STRATEGIES))
.child(Input::new("heights-field", self.state.clone()))
}
}
/// The trigger renders at the same height as the input field: the stated
/// metric plus the always-painted hairline border, whatever the platform.
#[test]
fn the_trigger_matches_the_input_height() {
let mut cx = TestAppContext::single();
cx.update(Theme::init);
let state = cx.update(InputState::new);
let state_for_window = state.clone();
let window = cx.add_window(move |_, _| HeightsHarness {
state: state_for_window,
});
let mut visual = VisualTestContext::from_window(window.into(), &cx);
draw(&mut visual);
let trigger = visual
.debug_bounds("select-trigger")
.expect("trigger rendered");
let field = visual.debug_bounds("input-field").expect("input rendered");
// Snapped bounds: allow half a pixel.
assert!((trigger.size.height - field.size.height).abs() <= px(0.5));
let expected = cx.update(|cx| cx.theme().sizes.medium.input_height) + BORDER_WIDTH * 2.0;
assert!((trigger.size.height - expected).abs() <= px(0.5));
}
+8 -8
View File
@@ -101,18 +101,18 @@ impl Default for Radius {
pub struct Borders { pub struct Borders {
/// Filled actions keep a border (Windows, KDE). /// Filled actions keep a border (Windows, KDE).
pub buttons: bool, pub buttons: bool,
/// Text fields and selects keep a border (Windows, KDE; the macOS /// Text fields and selects keep a border.
/// hairline bezel).
pub fields: bool, pub fields: bool,
/// Floating layers keep a border (Windows, KDE). /// Floating layers keep a border.
pub overlays: bool, pub overlays: bool,
/// Keyboard focus paints an accent ring around the control (macOS, /// Keyboard focus paints an accent ring around the control.
/// GNOME); otherwise focus re-colors the border (Windows, KDE).
pub focus_ring: bool, pub focus_ring: bool,
} }
impl Borders { impl Borders {
pub(crate) fn for_platform(source: SourceKind) -> Self { pub(crate) fn for_platform(source: SourceKind, variant: ThemeVariant) -> Self {
let overlays = variant == ThemeVariant::Dark;
match source { match source {
SourceKind::Windows | SourceKind::Kde => Self { SourceKind::Windows | SourceKind::Kde => Self {
buttons: true, buttons: true,
@@ -123,13 +123,13 @@ impl Borders {
SourceKind::Macos => Self { SourceKind::Macos => Self {
buttons: false, buttons: false,
fields: true, fields: true,
overlays: false, overlays,
focus_ring: true, focus_ring: true,
}, },
SourceKind::Omarchy | SourceKind::Gnome => Self { SourceKind::Omarchy | SourceKind::Gnome => Self {
buttons: false, buttons: false,
fields: false, fields: false,
overlays: false, overlays,
focus_ring: true, focus_ring: true,
}, },
SourceKind::Fallback => Self { SourceKind::Fallback => Self {
+14 -1
View File
@@ -275,7 +275,7 @@ pub(crate) fn resolve(
}, },
colors, colors,
radius, radius,
borders: Borders::for_platform(system.source), borders: Borders::for_platform(system.source, variant),
spacing: Spacing { spacing: Spacing {
unit: system.source.spacing_unit(), unit: system.source.spacing_unit(),
}, },
@@ -535,6 +535,19 @@ mod tests {
assert_eq!(unit, px(4.)); assert_eq!(unit, px(4.));
} }
#[test]
fn dark_mode_separates_overlays_by_border() {
for source in [SourceKind::Macos, SourceKind::Gnome, SourceKind::Omarchy] {
let mut stated = system(ThemeVariant::Dark, PartialPalette::default(), None);
stated.source = source;
let resolved = resolve(&stated, None, None);
assert!(
resolved.borders.overlays,
"{source:?} frames floating layers in dark"
);
}
}
#[test] #[test]
fn overlay_is_translucent_and_overridable() { fn overlay_is_translucent_and_overridable() {
let light = resolve( let light = resolve(
+10 -4
View File
@@ -9,9 +9,6 @@ is the first downstream consumer; its screens drive the order.
## Next ## Next
- [ ] **Select platform chrome** — the input treatment on the trigger; the
popup border behind `Theme::borders.overlays`.
## Queue — to follow ## Queue — to follow
Platform metric adoption continues one component at a time; each stop goes Platform metric adoption continues one component at a time; each stop goes
@@ -74,7 +71,9 @@ Gaps surfaced by the signed comparison; ordered by signed's usage.
metrics and the full radius ladder per platform, Linux root fonts metrics and the full radius ladder per platform, Linux root fonts
(GNOME gsettings `font-name`, KDE `kdeglobals font=`), per-source (GNOME gsettings `font-name`, KDE `kdeglobals font=`), per-source
border + focus chrome (`Theme::borders`), and a per-platform spacing border + focus chrome (`Theme::borders`), and a per-platform spacing
unit (`Theme::spacing`). Components adopt the tokens one by one unit (`Theme::spacing`); overlay borders engage in dark mode, where
the elevation shadow barely reads and floating layers separate by
border instead. Components adopt the tokens one by one
(see Next and Queue) (see Next and Queue)
### icons (`crates/icons`) ### icons (`crates/icons`)
@@ -103,6 +102,13 @@ Gaps surfaced by the signed comparison; ordered by signed's usage.
typically small ghost buttons; a docked click activates the control typically small ghost buttons; a docked click activates the control
without reaching the field's caret handler, and the text region shrinks without reaching the field's caret handler, and the text region shrinks
to make room to make room
- [x] Select platform chrome — the trigger takes the input treatment: the
hairline border always painted, sized outside the stated field height
and taking the fill's color where the platform paints none; open
recolors the border on Windows / KDE and paints the accent ring on
macOS / GNOME, so the trigger matches a focused field; the menu
popup's border sits behind `Theme::borders.overlays`, disappearing
into the surface where the platform paints none
- [x] Button, Checkbox, Switch, Radio + RadioGroup, Toggle, Link, Slider, - [x] Button, Checkbox, Switch, Radio + RadioGroup, Toggle, Link, Slider,
Progress, Avatar (image + seeded pixel-art fallback) Progress, Avatar (image + seeded pixel-art fallback)
- [x] Spinner — painted ring + rotating-icon variant, indeterminate and - [x] Spinner — painted ring + rotating-icon variant, indeterminate and