audit input

This commit is contained in:
2026-11-07 17:00:36 +07:00
parent fd36ada35e
commit ffbb732f5f
9 changed files with 437 additions and 144 deletions
+22 -9
View File
@@ -3,10 +3,11 @@
use gpui::prelude::FluentBuilder as _;
use gpui::{
AnyView, App, ClickEvent, ElementId, InteractiveElement as _, IntoElement, ParentElement as _,
RenderOnce, Role, SharedString, StatefulInteractiveElement as _, Styled as _, Window, relative,
RenderOnce, Role, SharedString, StatefulInteractiveElement as _, Styled as _, Window, hsla,
relative,
};
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 std::rc::Rc;
@@ -161,6 +162,7 @@ impl RenderOnce for Button {
let borders = theme.borders;
let metrics = theme.sizes.resolve(self.size);
let show_border = borders.buttons && self.variant != ButtonVariant::Ghost;
let on_click = self.on_click;
let a11y_label = self.a11y_label.or_else(|| self.label.clone());
@@ -191,6 +193,19 @@ impl RenderOnce for Button {
),
};
let (border, hover_border, pressed_border) = if show_border {
(colors.border, colors.border, colors.border)
} else {
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),
),
}
};
h_flex()
.id(self.id.clone())
.when_some(self.debug_selector, |button, selector| {
@@ -198,7 +213,7 @@ impl RenderOnce for Button {
})
.role(Role::Button)
.justify_center()
.h(metrics.control_height)
.h(metrics.control_height + BORDER_WIDTH * 2.0)
.when(self.icon.is_some(), |button| {
button.px(metrics.dense_padding_x)
})
@@ -207,10 +222,8 @@ impl RenderOnce for Button {
.gap_2()
.rounded(theme.radius.control)
.text_size(metrics.text_size)
.when(
borders.buttons && self.variant != ButtonVariant::Ghost,
|button| button.border_1().border_color(colors.border),
)
.border_1()
.border_color(border)
.when_some(background, |button, background| button.bg(background))
.text_color(foreground)
.when_some(a11y_label, |button, label| button.aria_label(label))
@@ -237,8 +250,8 @@ impl RenderOnce for Button {
.cursor_pointer()
.focusable()
.tab_stop(self.tab_stop)
.hover(move |style| style.bg(hover))
.active(move |style| style.bg(pressed))
.hover(move |style| style.bg(hover).border_color(hover_border))
.active(move |style| style.bg(pressed).border_color(pressed_border))
.focus_visible(|style| style.shadow(focus_ring(colors.ring)))
.when_some(on_click, |button, on_click| {
button.on_click(move |event, window, cx| on_click(event, window, cx))
+175 -94
View File
@@ -7,15 +7,17 @@ use unicode_segmentation::UnicodeSegmentation;
use gpui::prelude::FluentBuilder as _;
use gpui::{
App, AppContext as _, Bounds, ClipboardItem, ContentMask, Context, CursorStyle, DefiniteLength,
DispatchPhase, ElementId, Entity, EntityInputHandler, EventEmitter, Font, HitboxBehavior,
InteractiveElement as _, IntoElement, KeyBinding, KeyDownEvent, MouseButton, MouseDownEvent,
MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, RenderOnce, Role, ScrollDelta,
ScrollEvent, SharedString, StatefulInteractiveElement as _, Styled as _, Subscription, Task,
TextAlign, TextRun, UTF16Selection, Window, WrappedLine, actions, canvas, div, fill, point, px,
AnyElement, App, AppContext as _, Bounds, ClipboardItem, ContentMask, Context, CursorStyle,
DefiniteLength, DispatchPhase, ElementId, Entity, EntityInputHandler, EventEmitter, Font,
HitboxBehavior, InteractiveElement as _, IntoElement, KeyBinding, KeyDownEvent, MouseButton,
MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, RenderOnce, Role,
ScrollDelta, ScrollEvent, SharedString, StatefulInteractiveElement as _, Styled as _,
Subscription, Task, TextAlign, TextRun, UTF16Selection, Window, WrappedLine, actions, canvas,
div, fill, point, px,
};
use gpui_native_icons::IconRole;
use gpui_native_theme::{ActiveTheme as _, ColorTokens, Size};
use gpui_native_theme::{ActiveTheme as _, BORDER_WIDTH, ColorTokens, Size};
use gpui_native_utils::focus_ring;
use crate::button::{Button, ButtonVariant};
use crate::common::{Sizable, h_flex};
@@ -1904,6 +1906,8 @@ pub struct Input {
a11y_label: Option<SharedString>,
/// Multiline field height in rows; ignored when the state is single-line.
rows: usize,
prefix: Option<AnyElement>,
suffix: Option<AnyElement>,
}
impl Input {
@@ -1920,6 +1924,8 @@ impl Input {
disabled: false,
a11y_label: None,
rows: DEFAULT_ROWS,
prefix: None,
suffix: None,
}
}
@@ -1952,6 +1958,24 @@ impl Input {
self.a11y_label = Some(label.into());
self
}
/// Docks an element inside the field's leading edge.
pub fn prefix<E>(mut self, prefix: E) -> Self
where
E: IntoElement,
{
self.prefix = Some(prefix.into_any_element());
self
}
/// Docks an element inside the field's trailing edge.
pub fn suffix<E>(mut self, suffix: E) -> Self
where
E: IntoElement,
{
self.suffix = Some(suffix.into_any_element());
self
}
}
impl Sizable for Input {
@@ -1974,18 +1998,20 @@ impl RenderOnce for Input {
let revealed = self.state.read(cx).is_revealed();
let multiline = self.state.read(cx).is_multiline();
let right_padding = if is_password {
metrics.dense_padding_x + metrics.dense_icon_size
} else {
metrics.dense_padding_x
};
let small = theme.sizes.resolve(Size::Small);
let docked_height = small.control_height + BORDER_WIDTH * 2.0;
let edge_inset = ((metrics.input_height - docked_height) / 2.0).max(Pixels::ZERO);
// The field's own breathing room; the text starts here from the top when multiline.
let vertical_inset = metrics.dense_padding_x * 0.5;
let reveal_width = small.dense_padding_x * 2.0 + small.icon_size;
let field_padding = FieldPadding {
left: metrics.dense_padding_x,
right: right_padding,
left: Pixels::ZERO,
right: if is_password {
(edge_inset + reveal_width - metrics.dense_padding_x).max(Pixels::ZERO)
} else {
Pixels::ZERO
},
top: vertical_inset,
};
@@ -1993,9 +2019,10 @@ impl RenderOnce for Input {
DefiniteLength::Fraction(ratio) => metrics.text_size * ratio,
DefiniteLength::Absolute(length) => length.to_pixels(window.rem_size()),
});
let field_height = line_height.map_or(metrics.input_height, |line_height| {
line_height * self.rows as f32 + vertical_inset * 2.0
});
}) + BORDER_WIDTH * 2.0;
let fallback_label = self
.a11y_label
@@ -2003,8 +2030,20 @@ impl RenderOnce for Input {
.or_else(|| (!self.placeholder.is_empty()).then_some(self.placeholder.clone()))
.unwrap_or_else(|| SharedString::from(self.id.to_string()));
let edge_zone = |name: &'static str, element: AnyElement| {
div()
.debug_selector(|| name.to_string())
.flex_none()
.h_full()
.flex()
.items_center()
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
.child(element)
};
h_flex()
.id(self.id.clone())
.debug_selector(|| "input-field".to_string())
.key_context(INPUT_CONTEXT)
.role(Role::TextInput)
.aria_label(fallback_label)
@@ -2012,117 +2051,159 @@ impl RenderOnce for Input {
.overflow_hidden()
.w_full()
.h(field_height)
.px(metrics.dense_padding_x)
.pl(if self.prefix.is_some() {
edge_inset
} else {
metrics.dense_padding_x
})
.pr(if self.suffix.is_some() {
edge_inset
} else {
metrics.dense_padding_x
})
.gap_1p5()
.rounded(radius.control)
.border_1()
.border_color(colors.border)
.bg(colors.input)
.text_size(metrics.text_size)
.text_color(colors.foreground)
.when_some(line_height, |input, line_height| {
input.items_start().line_height(line_height)
.border_1()
.border_color(if theme.borders.fields {
colors.border
} else {
colors.input
})
.when(self.disabled, |input| input.opacity(0.5))
.when(!self.disabled, |input| {
input.track_focus(&focus_handle).tab_stop(true)
})
.when(focused, |input| input.border_color(colors.ring))
.when(is_empty && !focused, |input| {
input.child(
div()
.text_color(colors.muted_foreground)
.when(multiline, |placeholder| placeholder.pt(vertical_inset))
.child(self.placeholder.clone()),
)
.when(focused && !theme.borders.focus_ring, |input| {
input.border_color(colors.ring)
})
.when(focused && theme.borders.focus_ring, |input| {
input.shadow(focus_ring(colors.ring))
})
.when_some(self.prefix, |input, prefix| {
input.child(edge_zone("input-prefix", prefix))
})
.child(
canvas(
{
let state = self.state.clone();
move |bounds, window, cx| {
// The hitbox backs the I-beam hover cursor.
let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
let wrap_width = multiline.then(|| {
(bounds.size.width - field_padding.left - field_padding.right)
.max(Pixels::ZERO)
});
let text = state.read(cx).shape_current_text(wrap_width, window);
(text, hitbox)
}
},
{
let state = self.state.clone();
let focus = focus_handle.clone();
let disabled = self.disabled;
div()
.relative()
.flex_1()
.min_w_0()
.h_full()
.overflow_hidden()
.flex()
.flex_row()
.items_center()
.when_some(line_height, |region, line_height| {
region.items_start().line_height(line_height)
})
.when(is_empty && !focused, |region| {
region.child(
div()
.text_color(colors.muted_foreground)
.when(multiline, |placeholder| placeholder.pt(vertical_inset))
.child(self.placeholder.clone()),
)
})
.child(
canvas(
{
let state = self.state.clone();
move |bounds, window, cx| {
// The hitbox backs the I-beam hover cursor.
let hitbox =
window.insert_hitbox(bounds, HitboxBehavior::Normal);
let wrap_width = multiline.then(|| {
(bounds.size.width
- field_padding.left
- field_padding.right)
.max(Pixels::ZERO)
});
let text =
state.read(cx).shape_current_text(wrap_width, window);
(text, hitbox)
}
},
{
let state = self.state.clone();
let focus = focus_handle.clone();
let disabled = self.disabled;
move |bounds, (text, hitbox), window, cx| {
if !disabled {
window.set_cursor_style(CursorStyle::IBeam, &hitbox);
}
move |bounds, (text, hitbox), window, cx| {
if !disabled {
window.set_cursor_style(CursorStyle::IBeam, &hitbox);
}
let dragging = state.update(cx, |state, cx| {
state.paint(text, bounds, field_padding, colors, window, cx)
});
let dragging = state.update(cx, |state, cx| {
state.paint(text, bounds, field_padding, colors, window, cx)
});
if disabled {
return;
}
window.handle_input(
&focus,
gpui::ElementInputHandler::new(bounds, state.clone()),
cx,
);
window.on_mouse_event({
let state = state.clone();
move |event: &ScrollEvent, phase, window, cx| {
if phase != DispatchPhase::Bubble
|| !bounds.contains(&event.position)
{
if disabled {
return;
}
let delta = match event.delta {
ScrollDelta::Pixels(delta) => delta,
ScrollDelta::Lines(lines) => point(
px(lines.x * SCROLL_LINES_HEIGHT),
px(lines.y * SCROLL_LINES_HEIGHT),
),
};
window.handle_input(
&focus,
gpui::ElementInputHandler::new(bounds, state.clone()),
cx,
);
let consumed = state
.update(cx, |state, cx| state.scroll_by(delta, window, cx));
window.on_mouse_event({
let state = state.clone();
move |event: &ScrollEvent, phase, window, cx| {
if phase != DispatchPhase::Bubble
|| !bounds.contains(&event.position)
{
return;
}
if consumed {
cx.stop_propagation();
let delta = match event.delta {
ScrollDelta::Pixels(delta) => delta,
ScrollDelta::Lines(lines) => point(
px(lines.x * SCROLL_LINES_HEIGHT),
px(lines.y * SCROLL_LINES_HEIGHT),
),
};
let consumed = state.update(cx, |state, cx| {
state.scroll_by(delta, window, cx)
});
if consumed {
cx.stop_propagation();
}
}
});
if dragging {
InputState::register_drag_listeners(&state, window);
}
}
});
if dragging {
InputState::register_drag_listeners(&state, window);
}
}
},
)
.absolute()
.inset_0(),
},
)
.absolute()
.inset_0(),
),
)
.when_some(self.suffix, |input, suffix| {
input.child(edge_zone("input-suffix", suffix))
})
.when(is_password && !self.disabled, |input| {
input.child(
div()
.id((self.id.clone(), "input-reveal"))
.debug_selector(|| "input-reveal".to_string())
.absolute()
.right(metrics.dense_padding_x / 2.0)
.top_0p5()
.bottom_0p5()
.right(edge_inset)
.top_0()
.bottom_0()
.flex()
.items_center()
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
.child(
Button::new((self.id.clone(), "input-reveal-button"))
.debug_selector(|| "input-reveal-button".to_string())
.icon(if revealed {
IconRole::EyeOff
} else {
+9 -7
View File
@@ -5,7 +5,7 @@ use gpui::{
use gpui_native_core::button::{Button, ButtonVariant};
use gpui_native_core::input::{Input, InputState};
use gpui_native_theme::ActiveTheme as _;
use gpui_native_theme::Theme;
use gpui_native_theme::{BORDER_WIDTH, Theme};
struct Harness {
state: Entity<InputState>,
@@ -36,9 +36,10 @@ impl Render for Harness {
}
}
/// The medium input renders at the theme's stated field height and the
/// medium button at the stated control height; the two match wherever the
/// platform states equal values (macOS, Windows, GNOME).
/// The medium input and button render at the stated height plus the
/// hairline border sized outside it, so the two match wherever the platform
/// states equal values (macOS, Windows, GNOME) and stay equal to each other
/// on every platform.
#[gpui::test]
fn controls_render_at_the_stated_heights(cx: &mut TestAppContext) {
cx.update(Theme::init);
@@ -59,9 +60,10 @@ fn controls_render_at_the_stated_heights(cx: &mut TestAppContext) {
.debug_bounds("input-wrapper")
.expect("input rendered");
// Both bounds are snapped to device pixels, so allow half a pixel.
let input_height = cx.update(|cx| cx.theme().sizes.medium.input_height);
let button_height = cx.update(|cx| cx.theme().sizes.medium.control_height);
// Both bounds are snapped to device pixels, so allow half a pixel. The
// stated height is the interior; the border sizes outside it.
let input_height = cx.update(|cx| cx.theme().sizes.medium.input_height) + BORDER_WIDTH * 2.0;
let button_height = cx.update(|cx| cx.theme().sizes.medium.control_height) + BORDER_WIDTH * 2.0;
assert!((input.size.height - input_height).abs() <= gpui::px(0.5));
assert!((button.size.height - button_height).abs() <= gpui::px(0.5));
}
+163 -2
View File
@@ -3,12 +3,15 @@ use gpui::{
MouseUpEvent, ParentElement as _, Render, ScrollDelta, ScrollEvent, Styled as _,
TestAppContext, VisualTestContext, Window, div, point, px,
};
use gpui_native_core::button::{Button, ButtonVariant};
use gpui_native_core::common::Sizable as _;
use gpui_native_core::input::{
INPUT_CONTEXT, Input, InputBindings, InputState, InputStateEvent, Submit, SubmitOn,
bind_input_keys,
};
use gpui_native_theme::Theme;
use std::cell::RefCell;
use gpui_native_icons::IconRole;
use gpui_native_theme::{BORDER_WIDTH, Size, Theme};
use std::cell::{Cell, RefCell};
use std::rc::Rc;
use std::time::Duration;
@@ -763,6 +766,36 @@ fn the_reveal_toggle_flips_masking() {
assert!(!revealed, "the toggle should mask the field again");
}
/// The reveal pill sits with equal insets on its three free sides; the
/// small control metric must stay whole-pixel or snapping skews it.
#[test]
fn the_reveal_pill_insets_equally() {
let (cx, _state, mut visual) = reveal_window();
let field = visual.debug_bounds("input-field").expect("field missing");
let pill = visual
.debug_bounds("input-reveal-button")
.expect("reveal pill missing");
cx.update(|cx| {
let theme = Theme::global(cx);
let small = theme.sizes.resolve(Size::Small);
let border = BORDER_WIDTH;
let docked = small.control_height + border * 2.0;
let inset = ((field.size.height - border * 2.0 - docked) / 2.0).max(px(0.));
let top_gap = pill.top() - (field.top() + border);
let bottom_gap = (field.bottom() - border) - pill.bottom();
let right_gap = (field.right() - border) - pill.right();
assert!(
(top_gap - inset).abs() < px(0.01)
&& (bottom_gap - inset).abs() < px(0.01)
&& (right_gap - inset).abs() < px(0.01),
"pill gaps top {top_gap:?} bottom {bottom_gap:?} right {right_gap:?} vs inset {inset:?}"
);
});
}
#[test]
fn the_caret_can_rest_on_a_line_break() {
let (cx, state, mut visual) = multiline_window("ab\ncdef");
@@ -906,3 +939,131 @@ fn vertical_scroll_keeps_the_caret_in_view() {
.unwrap_or_default();
assert!(first_row_top < px(0.), "first row top: {first_row_top:?}");
}
struct EdgeHarness {
state: Entity<InputState>,
clicked: Rc<Cell<bool>>,
}
impl Render for EdgeHarness {
fn render(
&mut self,
_window: &mut Window,
_cx: &mut gpui::Context<Self>,
) -> impl gpui::IntoElement {
let clicked = self.clicked.clone();
div().w(px(400.)).p_4().child(
Input::new("edge-input", self.state.clone())
.placeholder("Search")
.prefix(
Button::new("edge-input-prefix-button")
.icon(IconRole::Search)
.variant(ButtonVariant::Ghost)
.small(),
)
.suffix(
Button::new("edge-input-suffix-button")
.debug_selector(|| "edge-suffix-button".to_string())
.icon(IconRole::Plus)
.variant(ButtonVariant::Ghost)
.small()
.on_click(move |_, _, _| clicked.set(true)),
),
)
}
}
fn edge_input_window(
initial: &str,
) -> (
TestAppContext,
Entity<InputState>,
VisualTestContext,
Rc<Cell<bool>>,
) {
let clicked = Rc::new(Cell::new(false));
let clicked_for_window = clicked.clone();
let mut cx = TestAppContext::single();
cx.update(|cx| {
Theme::init(cx);
bind_input_keys(cx, InputBindings::default());
});
let state = cx.update(|cx| InputState::new_with_text(initial, cx));
let state_for_window = state.clone();
let window = cx.add_window(move |_, _| EdgeHarness {
state: state_for_window,
clicked: clicked_for_window,
});
let mut visual = VisualTestContext::from_window(window.into(), &cx);
visual.update(|window, cx| {
window.draw(cx).clear(cx);
});
(cx, state, visual, clicked)
}
/// Prefix and suffix dock flush inside the field, prefix leading, and a
/// small button in a zone centers with equal insets like the reveal pill.
#[test]
fn prefix_and_suffix_dock_inside_the_field() {
let (cx, _state, mut visual, _clicked) = edge_input_window("abc");
let field = visual.debug_bounds("input-field").expect("field missing");
let prefix = visual
.debug_bounds("input-prefix")
.expect("prefix zone missing");
let suffix = visual
.debug_bounds("input-suffix")
.expect("suffix zone missing");
let pill = visual
.debug_bounds("edge-suffix-button")
.expect("suffix button missing");
assert!(prefix.left() >= field.left());
assert!(prefix.right() <= suffix.left());
assert!(suffix.right() <= field.right());
cx.update(|cx| {
let theme = Theme::global(cx);
let small = theme.sizes.resolve(Size::Small);
let border = BORDER_WIDTH;
let docked = small.control_height + border * 2.0;
let inset = ((field.size.height - border * 2.0 - docked) / 2.0).max(px(0.));
let top_gap = pill.top() - (field.top() + border);
let bottom_gap = (field.bottom() - border) - pill.bottom();
let right_gap = (field.right() - border) - pill.right();
assert!(
(top_gap - inset).abs() < px(0.01)
&& (bottom_gap - inset).abs() < px(0.01)
&& (right_gap - inset).abs() < px(0.01),
"suffix pill gaps top {top_gap:?} bottom {bottom_gap:?} right {right_gap:?} vs inset {inset:?}"
);
});
}
/// A click on a docked control activates it without running the field's
/// caret handler underneath.
#[test]
fn a_suffix_click_activates_without_focusing_the_field() {
let (cx, state, mut visual, clicked) = edge_input_window("abc");
let bounds = visual
.debug_bounds("input-suffix")
.expect("suffix zone missing");
click(
&mut visual,
point(
bounds.left() + bounds.size.width / 2.0,
bounds.top() + bounds.size.height / 2.0,
),
);
assert!(clicked.get(), "the suffix button should activate");
let focused = visual.update(|window, cx| state.read(cx).focus_handle().is_focused(window));
assert!(!focused, "the suffix click must not focus the field");
// The zone must not swallow text interaction either.
focus(&mut visual, &state);
visual.simulate_keystrokes("x");
assert_eq!(value(&cx, &state), "abcx");
}
+8 -4
View File
@@ -23,6 +23,10 @@ pub use system::{AccessibilityPreferences, PartialPalette, SourceKind, SystemThe
use gpui::{App, Global, Hsla, Pixels, WindowAppearance, 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.
pub const BORDER_WIDTH: Pixels = px(1.0);
/// Light or dark, resolved from the system appearance.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
@@ -459,17 +463,17 @@ impl SizeTable {
/// Sets the medium class's text-field height to the platform's stated value.
pub fn set_input_height(&mut self, medium_input_height: Pixels) {
let ratio = medium_input_height / self.medium.input_height;
self.small.input_height *= ratio;
self.small.input_height = (self.small.input_height * ratio).floor();
self.medium.input_height = medium_input_height;
self.large.input_height *= ratio;
self.large.input_height = (self.large.input_height * ratio).floor();
}
/// Sets the medium class's control height to the platform's stated value.
pub fn set_control_height(&mut self, medium_control_height: Pixels) {
let ratio = medium_control_height / self.medium.control_height;
self.small.control_height *= ratio;
self.small.control_height = (self.small.control_height * ratio).floor();
self.medium.control_height = medium_control_height;
self.large.control_height *= ratio;
self.large.control_height = (self.large.control_height * ratio).floor();
}
/// Sets the medium class's menu row height to the platform's stated value.
+6 -16
View File
@@ -449,17 +449,11 @@ mod tests {
let mut stated = system(ThemeVariant::Light, PartialPalette::default(), None);
stated.input_height = Some(px(24.));
let resolved = resolve(&stated, None, None);
// The medium class takes the stated height verbatim, and the other
// classes keep their derived ratio to the medium class.
assert_eq!(resolved.sizes.medium.input_height, px(24.));
// Derive what small would be without the statement, scale it by the
// stated-to-derived ratio, and compare.
let derived = SizeTable::with_root_font_size(px(16.));
let scale = px(24.) / derived.medium.input_height;
let expected_small = derived.small.input_height * scale;
assert!((resolved.sizes.small.input_height - expected_small).abs() < px(0.01));
let expected_small = (derived.small.input_height * scale).floor();
assert_eq!(resolved.sizes.small.input_height, expected_small);
}
#[test]
@@ -471,14 +465,10 @@ mod tests {
let derived = SizeTable::with_root_font_size(px(16.));
let scale = px(32.) / derived.medium.control_height;
assert!(
(resolved.sizes.small.control_height - derived.small.control_height * scale).abs()
< px(0.01)
);
assert!(
(resolved.sizes.large.control_height - derived.large.control_height * scale).abs()
< px(0.01)
);
let expected_small = (derived.small.control_height * scale).floor();
let expected_large = (derived.large.control_height * scale).floor();
assert_eq!(resolved.sizes.small.control_height, expected_small);
assert_eq!(resolved.sizes.large.control_height, expected_large);
}
#[test]
+2 -2
View File
@@ -35,9 +35,9 @@ pub fn focus_handle(id: &ElementId, window: &mut Window, cx: &mut App) -> FocusH
.clone()
}
/// Returns a focus ring shadow with the given color.
/// Returns a focus ring shadow with the given color at half ink.
pub fn focus_ring(color: Hsla) -> Vec<BoxShadow> {
vec![BoxShadow::new(px(0.), px(0.), color).spread_radius(px(3.))]
vec![BoxShadow::new(px(0.), px(0.), color.alpha(0.5)).spread_radius(px(3.))]
}
/// The next selectable row in the direction, or `None` at the ends.