Another project
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(ui): widgets module scaffolding

Lewis: May this revision serve well! <lu5a@proton.me>

+626 -6
+1
Cargo.toml
··· 27 27 expect_used = "deny" 28 28 missing_panics_doc = "allow" 29 29 missing_errors_doc = "allow" 30 + needless_for_each = "allow" 30 31 31 32 [workspace.dependencies] 32 33 bone-types = { path = "crates/bone-types" }
+4 -1
crates/bone-text/src/shape.rs
··· 25 25 } 26 26 } 27 27 28 - #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] 28 + #[derive( 29 + Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize, 30 + )] 31 + #[serde(transparent)] 29 32 pub struct SourceByteIndex(usize); 30 33 31 34 impl SourceByteIndex {
+32 -4
crates/bone-ui/src/frame.rs
··· 5 5 use crate::hotkey::{ActionId, HotkeyScopes, HotkeyTable, KeyChord}; 6 6 use crate::input::{InputSnapshot, KeyEvent}; 7 7 use crate::layout::LayoutRect; 8 + use crate::strings::StringTable; 8 9 use crate::theme::Theme; 9 10 use crate::widget_id::WidgetId; 10 11 ··· 59 60 pub input: &'a mut InputSnapshot, 60 61 pub focus: &'a mut FocusManager, 61 62 pub hotkeys: &'a HotkeyTable, 63 + pub strings: &'a StringTable, 62 64 pub hits: &'a mut HitFrame, 63 65 pub previous: &'a HitState, 64 66 } ··· 70 72 input: &'a mut InputSnapshot, 71 73 focus: &'a mut FocusManager, 72 74 hotkeys: &'a HotkeyTable, 75 + strings: &'a StringTable, 73 76 hits: &'a mut HitFrame, 74 77 previous: &'a HitState, 75 78 ) -> Self { ··· 80 83 input, 81 84 focus, 82 85 hotkeys, 86 + strings, 83 87 hits, 84 88 previous, 85 89 } 90 + } 91 + 92 + #[must_use] 93 + pub fn is_focused(&self, id: WidgetId) -> bool { 94 + self.focus.focused() == Some(id) 86 95 } 87 96 88 97 pub fn interact(&mut self, declaration: InteractDeclaration) -> Interaction { ··· 148 157 PointerButtonMask, PointerSample, 149 158 }; 150 159 use crate::layout::{LayoutPos, LayoutPx, LayoutRect, LayoutSize}; 160 + use crate::strings::StringTable; 151 161 use crate::theme::Theme; 152 162 use crate::widget_id::{WidgetId, WidgetKey}; 153 163 ··· 182 192 let prev = HitState::new(); 183 193 let mut input = InputSnapshot::idle(FrameInstant::ZERO); 184 194 { 185 - let mut frame = 186 - FrameCtx::new(theme, &mut input, &mut focus, &hotkeys, &mut hits, &prev); 195 + let mut frame = FrameCtx::new( 196 + theme, 197 + &mut input, 198 + &mut focus, 199 + &hotkeys, 200 + StringTable::empty(), 201 + &mut hits, 202 + &prev, 203 + ); 187 204 let _ = frame.interact( 188 205 InteractDeclaration::new(id("button"), rect(), Sense::INTERACTIVE).focusable(true), 189 206 ); ··· 216 233 &mut input, 217 234 &mut focus, 218 235 &table, 236 + StringTable::empty(), 219 237 &mut hits, 220 238 &prev, 221 239 ); ··· 240 258 &mut input, 241 259 &mut focus, 242 260 &table, 261 + StringTable::empty(), 243 262 &mut hits, 244 263 &prev, 245 264 ); ··· 269 288 &mut press, 270 289 &mut focus, 271 290 &hotkeys, 291 + StringTable::empty(), 272 292 &mut hits, 273 293 &state, 274 294 ); ··· 294 314 &mut release, 295 315 &mut focus, 296 316 &hotkeys, 317 + StringTable::empty(), 297 318 &mut hits, 298 319 &state, 299 320 ); ··· 307 328 hits.clear(); 308 329 let mut idle = InputSnapshot::idle(FrameInstant::from_duration(Duration::from_millis(80))); 309 330 { 310 - let mut frame = 311 - FrameCtx::new(theme, &mut idle, &mut focus, &hotkeys, &mut hits, &state); 331 + let mut frame = FrameCtx::new( 332 + theme, 333 + &mut idle, 334 + &mut focus, 335 + &hotkeys, 336 + StringTable::empty(), 337 + &mut hits, 338 + &state, 339 + ); 312 340 let _ = frame.interact( 313 341 InteractDeclaration::new(id("btn"), rect(), Sense::INTERACTIVE).focusable(true), 314 342 );
+1
crates/bone-ui/src/hit_test.rs
··· 31 31 32 32 impl ZLayer { 33 33 pub const BASE: Self = Self(0); 34 + pub const POPUP: Self = Self(100); 34 35 35 36 #[must_use] 36 37 pub const fn new(value: u32) -> Self {
+8
crates/bone-ui/src/hotkey.rs
··· 1 + use core::fmt; 1 2 use core::num::NonZeroU32; 2 3 use std::collections::BTreeMap; 3 4 ··· 24 25 key: event.code, 25 26 modifiers: event.modifiers, 26 27 } 28 + } 29 + } 30 + 31 + impl fmt::Display for KeyChord { 32 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 33 + fmt::Display::fmt(&self.modifiers, f)?; 34 + fmt::Display::fmt(&self.key, f) 27 35 } 28 36 } 29 37
+56
crates/bone-ui/src/input/key.rs
··· 1 + use core::fmt; 2 + 1 3 use serde::{Deserialize, Serialize}; 2 4 3 5 #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] ··· 29 31 } 30 32 } 31 33 34 + impl fmt::Display for ModifierMask { 35 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 36 + for (flag, name) in [ 37 + (Self::CTRL, "Ctrl"), 38 + (Self::SHIFT, "Shift"), 39 + (Self::ALT, "Alt"), 40 + (Self::META, "Meta"), 41 + ] { 42 + if self.contains(flag) { 43 + f.write_str(name)?; 44 + f.write_str("+")?; 45 + } 46 + } 47 + Ok(()) 48 + } 49 + } 50 + 32 51 #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] 33 52 pub enum NamedKey { 34 53 Tab, ··· 47 66 PageDown, 48 67 } 49 68 69 + impl NamedKey { 70 + #[must_use] 71 + pub const fn label(self) -> &'static str { 72 + match self { 73 + Self::Tab => "Tab", 74 + Self::Enter => "Enter", 75 + Self::Escape => "Esc", 76 + Self::Backspace => "Backspace", 77 + Self::Delete => "Delete", 78 + Self::Space => "Space", 79 + Self::ArrowUp => "Up", 80 + Self::ArrowDown => "Down", 81 + Self::ArrowLeft => "Left", 82 + Self::ArrowRight => "Right", 83 + Self::Home => "Home", 84 + Self::End => "End", 85 + Self::PageUp => "PageUp", 86 + Self::PageDown => "PageDown", 87 + } 88 + } 89 + } 90 + 91 + impl fmt::Display for NamedKey { 92 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 93 + f.write_str(self.label()) 94 + } 95 + } 96 + 50 97 #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] 51 98 #[serde(transparent)] 52 99 pub struct KeyChar(char); ··· 67 114 pub enum KeyCode { 68 115 Named(NamedKey), 69 116 Char(KeyChar), 117 + } 118 + 119 + impl fmt::Display for KeyCode { 120 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 121 + match self { 122 + Self::Named(named) => fmt::Display::fmt(named, f), 123 + Self::Char(c) => f.write_str(&c.get().to_uppercase().collect::<String>()), 124 + } 125 + } 70 126 } 71 127 72 128 #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
+2
crates/bone-ui/src/input/mod.rs
··· 16 16 pub buttons_pressed: PointerButtonMask, 17 17 pub buttons_released: PointerButtonMask, 18 18 pub keys_pressed: Vec<KeyEvent>, 19 + pub text_committed: String, 19 20 pub double_click_window: DoubleClickWindow, 20 21 pub drag_threshold: DragThreshold, 21 22 } ··· 29 30 buttons_pressed: PointerButtonMask::EMPTY, 30 31 buttons_released: PointerButtonMask::EMPTY, 31 32 keys_pressed: Vec::new(), 33 + text_committed: String::new(), 32 34 double_click_window: DoubleClickWindow::DEFAULT, 33 35 drag_threshold: DragThreshold::DEFAULT, 34 36 }
+1
crates/bone-ui/src/lib.rs
··· 8 8 pub mod text; 9 9 pub mod theme; 10 10 mod widget_id; 11 + pub mod widgets; 11 12 12 13 pub use focus::{ 13 14 FocusManager, FocusRequest, FocusScopeId, FocusScopeKind, InputModality, RovingDirection,
+11 -1
crates/bone-ui/src/strings.rs
··· 1 1 use core::fmt; 2 2 use std::collections::HashMap; 3 + use std::sync::LazyLock; 3 4 4 - #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] 5 + use serde::Serialize; 6 + 7 + #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize)] 8 + #[serde(transparent)] 5 9 pub struct StringKey(&'static str); 6 10 7 11 impl StringKey { ··· 31 35 #[must_use] 32 36 pub fn new() -> Self { 33 37 Self::default() 38 + } 39 + 40 + #[must_use] 41 + pub fn empty() -> &'static Self { 42 + static EMPTY: LazyLock<StringTable> = LazyLock::new(StringTable::default); 43 + &EMPTY 34 44 } 35 45 36 46 #[must_use]
+87
crates/bone-ui/src/widgets/keys.rs
··· 1 + use crate::input::{InputSnapshot, KeyCode, KeyEvent, ModifierMask, NamedKey}; 2 + 3 + #[derive(Copy, Clone, Debug, PartialEq, Eq)] 4 + pub struct TakeKey { 5 + pub code: KeyCode, 6 + pub modifiers: ModifierMask, 7 + } 8 + 9 + impl TakeKey { 10 + #[must_use] 11 + pub const fn new(code: KeyCode, modifiers: ModifierMask) -> Self { 12 + Self { code, modifiers } 13 + } 14 + 15 + #[must_use] 16 + pub const fn named(key: NamedKey) -> Self { 17 + Self::new(KeyCode::Named(key), ModifierMask::NONE) 18 + } 19 + 20 + fn matches(self, event: KeyEvent) -> bool { 21 + event.code == self.code && event.modifiers == self.modifiers 22 + } 23 + } 24 + 25 + #[must_use] 26 + pub fn take_key(input: &mut InputSnapshot, candidates: &[TakeKey]) -> Option<KeyEvent> { 27 + let position = input 28 + .keys_pressed 29 + .iter() 30 + .position(|event| candidates.iter().any(|target| target.matches(*event))); 31 + position.map(|index| input.keys_pressed.remove(index)) 32 + } 33 + 34 + pub(super) fn take_activation(input: &mut InputSnapshot) -> bool { 35 + take_key( 36 + input, 37 + &[ 38 + TakeKey::named(NamedKey::Enter), 39 + TakeKey::named(NamedKey::Space), 40 + ], 41 + ) 42 + .is_some() 43 + } 44 + 45 + #[cfg(test)] 46 + mod tests { 47 + use super::{TakeKey, take_key}; 48 + use crate::input::{ 49 + FrameInstant, InputSnapshot, KeyChar, KeyCode, KeyEvent, ModifierMask, NamedKey, 50 + }; 51 + 52 + fn snap_with(events: Vec<KeyEvent>) -> InputSnapshot { 53 + let mut snap = InputSnapshot::idle(FrameInstant::ZERO); 54 + snap.keys_pressed = events; 55 + snap 56 + } 57 + 58 + #[test] 59 + fn take_key_drains_first_match_and_keeps_rest() { 60 + let enter = KeyEvent::new(KeyCode::Named(NamedKey::Enter), ModifierMask::NONE); 61 + let other = KeyEvent::new(KeyCode::Char(KeyChar::from_char('x')), ModifierMask::NONE); 62 + let mut snap = snap_with(vec![other, enter, other]); 63 + let taken = take_key(&mut snap, &[TakeKey::named(NamedKey::Enter)]); 64 + assert_eq!(taken, Some(enter)); 65 + assert_eq!(snap.keys_pressed, vec![other, other]); 66 + } 67 + 68 + #[test] 69 + fn take_key_returns_none_on_no_match() { 70 + let other = KeyEvent::new(KeyCode::Char(KeyChar::from_char('x')), ModifierMask::NONE); 71 + let mut snap = snap_with(vec![other]); 72 + let taken = take_key(&mut snap, &[TakeKey::named(NamedKey::Enter)]); 73 + assert!(taken.is_none()); 74 + assert_eq!(snap.keys_pressed, vec![other]); 75 + } 76 + 77 + #[test] 78 + fn take_key_respects_modifier_mask() { 79 + let plain = KeyEvent::new(KeyCode::Char(KeyChar::from_char('s')), ModifierMask::NONE); 80 + let ctrl = KeyEvent::new(KeyCode::Char(KeyChar::from_char('s')), ModifierMask::CTRL); 81 + let mut snap = snap_with(vec![plain, ctrl]); 82 + let target = TakeKey::new(KeyCode::Char(KeyChar::from_char('s')), ModifierMask::CTRL); 83 + let taken = take_key(&mut snap, &[target]); 84 + assert_eq!(taken, Some(ctrl)); 85 + assert_eq!(snap.keys_pressed, vec![plain]); 86 + } 87 + }
+46
crates/bone-ui/src/widgets/mod.rs
··· 1 + mod button; 2 + mod checkbox; 3 + mod dimensioned_input; 4 + mod dropdown; 5 + mod hotkey_capture; 6 + mod keys; 7 + mod numeric_input; 8 + mod paint; 9 + mod parsed_input; 10 + mod radio_group; 11 + mod slider; 12 + mod text_input; 13 + mod toggle_button; 14 + mod tooltip; 15 + mod visuals; 16 + 17 + pub use button::{ 18 + Button, ButtonResponse, ButtonState, ButtonVariant, ButtonVisuals, button_visuals, show_button, 19 + }; 20 + pub use checkbox::{Checkbox, CheckboxResponse, CheckboxState, show_checkbox}; 21 + pub use dimensioned_input::{DimensionedInput, DimensionedInputResponse, DimensionedParseError}; 22 + pub use dropdown::{Dropdown, DropdownItem, DropdownResponse, DropdownState, show_dropdown}; 23 + pub use hotkey_capture::{ 24 + HotkeyCapture, HotkeyCaptureResponse, HotkeyCaptureState, show_hotkey_capture, 25 + }; 26 + pub use keys::{TakeKey, take_key}; 27 + pub use numeric_input::{NumericFloatParseError, NumericInput, NumericInputResponse}; 28 + pub use paint::{ButtonPaintKind, GlyphMark, LabelText, SelectionByteRange, WidgetPaint}; 29 + pub use parsed_input::{ParsedInput, ParsedInputResponse, ParsedValue, show_parsed_input}; 30 + pub use radio_group::{ 31 + RadioGroup, RadioGroupResponse, RadioOption, RadioOrientation, show_radio_group, 32 + }; 33 + pub use slider::{ 34 + Slider, SliderCoarseStep, SliderRange, SliderRangeError, SliderResponse, SliderScalar, 35 + SliderStep, SliderStepError, show_slider, 36 + }; 37 + pub use text_input::{ 38 + AlwaysValid, Clipboard, MemoryClipboard, TextInput, TextInputAction, TextInputEdit, 39 + TextInputResponse, TextInputState, TextInputValidation, show_text_input, 40 + }; 41 + pub use toggle_button::{ToggleButton, ToggleButtonResponse, show_toggle_button}; 42 + pub use tooltip::{Tooltip, TooltipPlacement, TooltipState, show_tooltip}; 43 + pub use visuals::{ 44 + FieldVisuals, Indicator, SurfaceVisuals, TextVisuals, indicator_border, indicator_fill, 45 + indicator_label_color, push_focus_ring, push_indicator, 46 + };
+90
crates/bone-ui/src/widgets/paint.rs
··· 1 + use serde::Serialize; 2 + 3 + use crate::layout::LayoutRect; 4 + use crate::strings::StringKey; 5 + use crate::text::SourceByteIndex; 6 + use crate::theme::{Border, Color, ElevationLevel, Radius, Spacing, TypographyRole}; 7 + use crate::widget_id::WidgetId; 8 + 9 + #[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize)] 10 + pub enum ButtonPaintKind { 11 + Filled, 12 + Outlined, 13 + Ghost, 14 + Danger, 15 + IconOnly, 16 + } 17 + 18 + #[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize)] 19 + pub enum GlyphMark { 20 + Checkmark, 21 + Indeterminate, 22 + RadioDot, 23 + Caret, 24 + Chevron, 25 + SliderThumb, 26 + Spinner, 27 + } 28 + 29 + #[derive(Clone, Debug, PartialEq, Eq, Serialize)] 30 + pub enum LabelText { 31 + Key(StringKey), 32 + Owned(String), 33 + } 34 + 35 + #[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize)] 36 + pub struct SelectionByteRange { 37 + pub min: SourceByteIndex, 38 + pub max: SourceByteIndex, 39 + } 40 + 41 + impl SelectionByteRange { 42 + #[must_use] 43 + pub const fn new(min: SourceByteIndex, max: SourceByteIndex) -> Self { 44 + Self { min, max } 45 + } 46 + } 47 + 48 + #[derive(Clone, Debug, PartialEq, Serialize)] 49 + pub enum WidgetPaint { 50 + Surface { 51 + rect: LayoutRect, 52 + fill: Color, 53 + border: Option<Border>, 54 + radius: Radius, 55 + elevation: Option<ElevationLevel>, 56 + }, 57 + Label { 58 + rect: LayoutRect, 59 + text: LabelText, 60 + color: Color, 61 + role: TypographyRole, 62 + }, 63 + Mark { 64 + rect: LayoutRect, 65 + kind: GlyphMark, 66 + color: Color, 67 + }, 68 + FocusRing { 69 + rect: LayoutRect, 70 + color: Color, 71 + radius: Radius, 72 + thickness: Spacing, 73 + }, 74 + SelectionHighlight { 75 + rect: LayoutRect, 76 + range: SelectionByteRange, 77 + color: Color, 78 + }, 79 + Caret { 80 + rect: LayoutRect, 81 + byte_offset: SourceByteIndex, 82 + color: Color, 83 + }, 84 + Tooltip { 85 + rect: LayoutRect, 86 + text: LabelText, 87 + anchor: WidgetId, 88 + elevation: ElevationLevel, 89 + }, 90 + }
+130
crates/bone-ui/src/widgets/parsed_input.rs
··· 1 + use core::fmt::Display; 2 + use core::marker::PhantomData; 3 + 4 + use crate::frame::FrameCtx; 5 + use crate::input::NamedKey; 6 + use crate::layout::LayoutRect; 7 + use crate::strings::StringKey; 8 + use crate::widget_id::WidgetId; 9 + 10 + use super::keys::{TakeKey, take_key}; 11 + use super::paint::WidgetPaint; 12 + use super::text_input::{ 13 + Clipboard, TextInput, TextInputResponse, TextInputState, TextInputValidation, 14 + show_text_input, 15 + }; 16 + 17 + pub trait ParsedValue: Sized + Clone + PartialEq { 18 + type Error: Display + Clone + PartialEq; 19 + 20 + fn parse(text: &str) -> Result<Self, Self::Error>; 21 + } 22 + 23 + pub struct ParsedInput<'state, T: ParsedValue> { 24 + pub id: WidgetId, 25 + pub rect: LayoutRect, 26 + pub placeholder: StringKey, 27 + pub state: &'state mut TextInputState, 28 + pub disabled: bool, 29 + _ty: PhantomData<T>, 30 + } 31 + 32 + impl<'state, T: ParsedValue> ParsedInput<'state, T> { 33 + #[must_use] 34 + pub fn new( 35 + id: WidgetId, 36 + rect: LayoutRect, 37 + placeholder: StringKey, 38 + state: &'state mut TextInputState, 39 + ) -> Self { 40 + Self { 41 + id, 42 + rect, 43 + placeholder, 44 + state, 45 + disabled: false, 46 + _ty: PhantomData, 47 + } 48 + } 49 + 50 + #[must_use] 51 + pub fn disabled(self, disabled: bool) -> Self { 52 + Self { disabled, ..self } 53 + } 54 + } 55 + 56 + #[derive(Clone, Debug, PartialEq)] 57 + pub struct ParsedInputResponse<T: ParsedValue> { 58 + pub interaction: crate::hit_test::Interaction, 59 + pub value: Option<T>, 60 + pub committed: Option<T>, 61 + pub paint: Vec<WidgetPaint>, 62 + pub error: Option<T::Error>, 63 + } 64 + 65 + struct ParsedValidator<T: ParsedValue>(PhantomData<T>); 66 + 67 + impl<T: ParsedValue> TextInputValidation for ParsedValidator<T> { 68 + type Error = T::Error; 69 + fn validate(&self, text: &str) -> Result<(), Self::Error> { 70 + if text.trim().is_empty() { 71 + return Ok(()); 72 + } 73 + T::parse(text).map(drop) 74 + } 75 + } 76 + 77 + #[must_use] 78 + pub fn show_parsed_input<T: ParsedValue, C: Clipboard>( 79 + ctx: &mut FrameCtx<'_>, 80 + input: ParsedInput<'_, T>, 81 + clipboard: &mut C, 82 + ) -> ParsedInputResponse<T> { 83 + let ParsedInput { 84 + id, 85 + rect, 86 + placeholder, 87 + state, 88 + disabled, 89 + .. 90 + } = input; 91 + let was_focused = state.was_focused; 92 + let live_focused = ctx.is_focused(id); 93 + let commit_via_enter = !disabled 94 + && live_focused 95 + && take_key(ctx.input, &[TakeKey::named(NamedKey::Enter)]).is_some(); 96 + let TextInputResponse { 97 + interaction, 98 + error, 99 + paint, 100 + .. 101 + } = { 102 + let widget = TextInput { 103 + id, 104 + rect, 105 + placeholder, 106 + state: &mut *state, 107 + disabled, 108 + validator: ParsedValidator::<T>(PhantomData), 109 + }; 110 + show_text_input(ctx, widget, clipboard) 111 + }; 112 + let value = if state.text.trim().is_empty() { 113 + None 114 + } else { 115 + T::parse(&state.text).ok() 116 + }; 117 + let lost_focus = was_focused && !live_focused; 118 + let committed = if commit_via_enter || lost_focus { 119 + value.clone() 120 + } else { 121 + None 122 + }; 123 + ParsedInputResponse { 124 + interaction, 125 + value, 126 + committed, 127 + paint, 128 + error, 129 + } 130 + }
+157
crates/bone-ui/src/widgets/visuals.rs
··· 1 + use serde::Serialize; 2 + 3 + use crate::frame::FrameCtx; 4 + use crate::hit_test::Interaction; 5 + use crate::layout::LayoutRect; 6 + use crate::strings::StringKey; 7 + use crate::theme::{ 8 + Border, Color, ElevationLevel, Radius, Spacing, Step12, StrokeWidth, Theme, TypographyRole, 9 + }; 10 + 11 + use super::paint::{GlyphMark, LabelText, WidgetPaint}; 12 + 13 + #[derive(Copy, Clone, Debug, PartialEq, Serialize)] 14 + pub struct SurfaceVisuals { 15 + pub fill: Color, 16 + pub border: Option<Border>, 17 + pub radius: Radius, 18 + pub elevation: Option<ElevationLevel>, 19 + } 20 + 21 + #[derive(Copy, Clone, Debug, PartialEq, Serialize)] 22 + pub struct TextVisuals { 23 + pub color: Color, 24 + pub role: TypographyRole, 25 + } 26 + 27 + #[derive(Copy, Clone, Debug, PartialEq, Serialize)] 28 + pub struct FieldVisuals { 29 + pub surface: SurfaceVisuals, 30 + pub text: TextVisuals, 31 + pub placeholder: Color, 32 + pub caret: Color, 33 + pub selection: Color, 34 + } 35 + 36 + pub fn push_focus_ring( 37 + ctx: &FrameCtx<'_>, 38 + paint: &mut Vec<WidgetPaint>, 39 + rect: LayoutRect, 40 + radius: Radius, 41 + live_focused: bool, 42 + ) { 43 + if live_focused && ctx.focus.focus_visible() { 44 + paint.push(WidgetPaint::FocusRing { 45 + rect, 46 + color: ctx.theme.colors.focus_ring, 47 + radius, 48 + thickness: Spacing::px(StrokeWidth::HAIRLINE.value_px() * 2.0), 49 + }); 50 + } 51 + } 52 + 53 + #[must_use] 54 + pub fn indicator_fill( 55 + theme: &Theme, 56 + active: bool, 57 + disabled: bool, 58 + interaction: Interaction, 59 + ) -> Color { 60 + let neutral = theme.colors.neutral; 61 + let accent = theme.colors.accent; 62 + let pressed = interaction.pressed(); 63 + let hovered = interaction.hover(); 64 + if disabled { 65 + neutral.step(Step12::SUBTLE_BG) 66 + } else if active { 67 + if pressed || hovered { 68 + accent.step(Step12::HOVER_SOLID) 69 + } else { 70 + accent.step(Step12::SOLID) 71 + } 72 + } else if pressed { 73 + neutral.step(Step12::SELECTED_BG) 74 + } else if hovered { 75 + neutral.step(Step12::HOVER_BG) 76 + } else { 77 + neutral.step(Step12::ELEMENT_BG) 78 + } 79 + } 80 + 81 + #[must_use] 82 + pub fn indicator_border(theme: &Theme, active: bool, hovered: bool) -> Option<Border> { 83 + (!active).then(|| Border { 84 + width: StrokeWidth::HAIRLINE, 85 + color: theme.colors.neutral.step(if hovered { 86 + Step12::HOVER_BORDER 87 + } else { 88 + Step12::BORDER 89 + }), 90 + }) 91 + } 92 + 93 + #[must_use] 94 + pub fn indicator_label_color( 95 + theme: &Theme, 96 + surface_fill: Color, 97 + active: bool, 98 + disabled: bool, 99 + ) -> Color { 100 + if disabled { 101 + theme.colors.text_disabled() 102 + } else if active { 103 + surface_fill.on_surface() 104 + } else { 105 + theme.colors.text_primary() 106 + } 107 + } 108 + 109 + #[derive(Copy, Clone, Debug, PartialEq)] 110 + pub struct Indicator { 111 + pub rect: LayoutRect, 112 + pub label: StringKey, 113 + pub mark: Option<GlyphMark>, 114 + pub active: bool, 115 + pub disabled: bool, 116 + pub radius: Radius, 117 + } 118 + 119 + pub fn push_indicator( 120 + ctx: &FrameCtx<'_>, 121 + paint: &mut Vec<WidgetPaint>, 122 + indicator: Indicator, 123 + interaction: Interaction, 124 + live_focused: bool, 125 + ) { 126 + let Indicator { 127 + rect, 128 + label, 129 + mark, 130 + active, 131 + disabled, 132 + radius, 133 + } = indicator; 134 + let fill = indicator_fill(&ctx.theme, active, disabled, interaction); 135 + let border = indicator_border(&ctx.theme, active, interaction.hover()); 136 + paint.push(WidgetPaint::Surface { 137 + rect, 138 + fill, 139 + border, 140 + radius, 141 + elevation: None, 142 + }); 143 + paint.push(WidgetPaint::Label { 144 + rect, 145 + text: LabelText::Key(label), 146 + color: indicator_label_color(&ctx.theme, fill, active, disabled), 147 + role: ctx.theme.typography.label, 148 + }); 149 + if let Some(kind) = mark { 150 + paint.push(WidgetPaint::Mark { 151 + rect, 152 + kind, 153 + color: fill.on_surface(), 154 + }); 155 + } 156 + push_focus_ring(ctx, paint, rect, radius, live_focused); 157 + }