diff --git a/examples/showcase.rs b/examples/showcase.rs index 3b3bfeb..ad01a1c 100644 --- a/examples/showcase.rs +++ b/examples/showcase.rs @@ -97,16 +97,19 @@ impl App for ShowcaseApp button( "Primary" ) .variant( ButtonVariant::Primary ) .id( Self::SAVE_BUTTON_ID ) + .tooltip( "Hover-dwell tooltip via .tooltip()" ) .on_press( Message::Pressed( "Primary" ) ), ) .push( button( "Secondary" ) .variant( ButtonVariant::Secondary ) + .tooltip( "Secondary action" ) .on_press( Message::Pressed( "Secondary" ) ), ) .push( button( "Tertiary" ) .variant( ButtonVariant::Tertiary ) + .tooltip( "Tertiary action" ) .on_press( Message::Pressed( "Tertiary" ) ), ); diff --git a/src/draw/damage.rs b/src/draw/damage.rs index 32e8a9b..de53493 100644 --- a/src/draw/damage.rs +++ b/src/draw/damage.rs @@ -233,6 +233,7 @@ mod tests handlers: WidgetHandlers::None, keyboard_focusable: true, cursor: crate::types::CursorShape::Default, + tooltip: None, } } diff --git a/src/draw/layout.rs b/src/draw/layout.rs index a85d07d..e0ba212 100644 --- a/src/draw/layout.rs +++ b/src/draw/layout.rs @@ -129,6 +129,7 @@ pub( crate ) fn layout_and_draw( }, keyboard_focusable: false, cursor: p.cursor.unwrap_or( crate::types::CursorShape::Pointer ), + tooltip: None, } ); } layout_and_draw::( p.child.as_ref(), canvas, rect, ctx, my_idx + 1 ) @@ -378,6 +379,7 @@ pub( crate ) fn layout_and_draw( handlers: other.handlers(), keyboard_focusable: other.is_focusable(), cursor: other.cursor_shape(), + tooltip: other.tooltip().map( str::to_string ), } ); } flat_idx + 1 diff --git a/src/event_loop/app_data.rs b/src/event_loop/app_data.rs index c7ea1b4..a563098 100644 --- a/src/event_loop/app_data.rs +++ b/src/event_loop/app_data.rs @@ -239,6 +239,26 @@ pub( crate ) enum SurfaceFocus Overlay( OverlayId ), } +pub const TOOLTIP_DELAY: std::time::Duration = std::time::Duration::from_millis( 600 ); + +#[derive( Clone )] +pub struct TooltipPending +{ + pub focus: SurfaceFocus, + pub flat_idx: usize, + pub deadline: std::time::Instant, + pub text: String, + pub anchor: crate::types::Rect, +} + +#[derive( Clone )] +pub struct TooltipVisible +{ + pub focus: SurfaceFocus, + pub anchor: crate::types::Rect, + pub text: String, +} + /// Configuration for a layer-shell surface, used both for the main surface /// (when the app uses [`crate::app::ShellMode::Layer`]) and for each overlay /// returned by [`crate::app::App::overlays`]. @@ -415,6 +435,7 @@ pub( crate ) struct SurfaceState /// for slide-down / grow animations). `(0, 0)` for non-layer surfaces /// and for layer surfaces before their first configure. pub last_requested_size: ( u32, u32 ), + pub layer_anchor: Option, /// Anchor rect the xdg-popup positioner was last configured with. /// `None` for non-popup surfaces. pub last_popup_anchor: Option, @@ -465,6 +486,7 @@ impl SurfaceState titlebar_close_rect: Rect::default(), scale_factor: 1, last_requested_size: ( 0, 0 ), + layer_anchor: None, last_popup_anchor: None, popup_reposition_token: 0, frame_pending: false, @@ -674,6 +696,9 @@ pub struct AppData /// before the user's finger / cursor moves — useful on mouse where the /// pointer might sit perfectly still between press and drag. pub pending_drag_inits: Vec, + + pub tooltip_pending: Option, + pub tooltip_visible: Option, pub qh: QueueHandle, /// Last pointer serial (needed for interactive move). pub last_pointer_serial: u32, @@ -1894,6 +1919,120 @@ impl AppData soonest } + pub( crate ) fn arm_tooltip( &mut self, focus: SurfaceFocus, flat_idx: usize ) + { + let ss = self.surface( focus ); + let Some( w ) = crate::tree::find_widget( &ss.widget_rects, flat_idx ) else { return }; + let Some( text ) = w.tooltip.clone() else + { + self.tooltip_pending = None; + return; + }; + let anchor = w.rect; + if let Some( ref p ) = self.tooltip_pending + { + if p.focus == focus && p.flat_idx == flat_idx { return; } + } + self.tooltip_pending = Some( TooltipPending + { + focus, + flat_idx, + deadline: std::time::Instant::now() + TOOLTIP_DELAY, + text, + anchor, + } ); + if self.tooltip_visible.is_some() + { + self.tooltip_visible = None; + self.overlays_dirty = true; + } + } + + pub( crate ) fn cancel_tooltip( &mut self ) + { + let was_visible = self.tooltip_visible.take().is_some(); + self.tooltip_pending = None; + if was_visible { self.overlays_dirty = true; } + } + + pub( crate ) fn next_tooltip_wakeup( &self ) -> Option + { + let p = self.tooltip_pending.as_ref()?; + Some( p.deadline.saturating_duration_since( std::time::Instant::now() ) ) + } + + pub( crate ) fn check_tooltip_deadline( &mut self ) + { + let Some( p ) = self.tooltip_pending.as_ref() else { return }; + if std::time::Instant::now() < p.deadline { return; } + let p = self.tooltip_pending.take().unwrap(); + self.tooltip_visible = Some( TooltipVisible + { + focus: p.focus, + anchor: p.anchor, + text: p.text, + } ); + self.overlays_dirty = true; + self.main.request_redraw(); + } + + pub( crate ) fn tooltip_overlay( &self ) -> Option> + { + let v = self.tooltip_visible.as_ref()?; + let ( ox, oy ) = self.surface_offset_for( v.focus ); + let scale = self.surface( v.focus ).scale_factor as f32; + let anchor_x = ox + v.anchor.x / scale; + let anchor_y = oy + v.anchor.y / scale; + let anchor_w = v.anchor.width / scale; + let anchor_h = v.anchor.height / scale; + + let palette = crate::theme::palette(); + let bg_col = crate::types::Color::rgba( palette.text_primary.r, palette.text_primary.g, palette.text_primary.b, 0.95 ); + let pill: crate::widget::Element = crate::widget::container( + crate::widget::text( v.text.clone() ) + .size( 13.0 ) + .color( palette.bg ) + ) + .background( bg_col ) + .padding_h( 12.0 ) + .padding_v( 6.0 ) + .radius( 8.0 ) + .into(); + + let estimated_w = ( v.text.chars().count() as f32 * 7.5 + 24.0 ).clamp( 40.0, 320.0 ); + let estimated_h = 28.0_f32; + let mut x = anchor_x + ( anchor_w - estimated_w ) / 2.0; + let mut y = anchor_y - estimated_h - 6.0; + let sw = self.main.width as f32; + let sh = self.main.height as f32; + x = x.clamp( 4.0, ( sw - estimated_w - 4.0 ).max( 4.0 ) ); + if y < 4.0 { y = anchor_y + anchor_h + 6.0; } + if y + estimated_h > sh - 4.0 { y = ( sh - estimated_h - 4.0 ).max( 4.0 ); } + + let view: crate::widget::Element = crate::layout::stack::stack() + .push_translated( pill, crate::layout::stack::HAlign::Start, crate::layout::stack::VAlign::Top, x, y ) + .into(); + + use std::hash::{ Hash, Hasher }; + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + "ltk-tooltip".hash( &mut hasher ); + let id = crate::app::OverlayId( hasher.finish() as u32 ); + + Some( crate::app::OverlaySpec + { + id, + layer: crate::app::Layer::Overlay, + anchor: crate::app::Anchor::ALL, + size: ( 0, 0 ), + exclusive_zone: -1, + keyboard_exclusive: false, + input_region: Some( Vec::new() ), + view, + on_dismiss: None, + anchor_widget_id: None, + } ) + } + /// Fire long-press messages for any surface whose deadline has /// elapsed. Idempotent — if a press is in-flight but not yet due this /// does nothing. On fire, the message is pushed to `pending_msgs`, @@ -1960,7 +2099,9 @@ impl AppData if let Some( m ) = ds_msg { self.pending_msgs.push( m ); - self.pending_drag_inits.push( origin ); + let ( ox, oy ) = self.surface_offset_for( focus ); + let global = crate::Point { x: origin.x + ox, y: origin.y + oy }; + self.pending_drag_inits.push( global ); // Drag promotion cancels any held-button repeat — the // gesture has switched semantics and the timer has // nothing to fire against any more. diff --git a/src/event_loop/handlers.rs b/src/event_loop/handlers.rs index 6993df0..117c575 100644 --- a/src/event_loop/handlers.rs +++ b/src/event_loop/handlers.rs @@ -412,6 +412,23 @@ impl ProvidesRegistryState for AppData // handle proxy — unique per session and stable for the handle's // lifetime, the same value paired across `Opened` and the matching // `Closed`. +/// Cascade for the per-toplevel display string crustace-style shells use to +/// register a window: prefer `app_id` (sets `.desktop` matching), fall back +/// to `title` (visible to the user), and finally to the protocol-issued +/// `identifier` (always present, unique per handle). Without the cascade, +/// any client that never set `app_id` (e.g. winit-windowed compositors +/// before they bind their xdg surface) was silently invisible to the dock. +fn toplevel_display_id( + list: &ForeignToplevelList, + handle: &ExtForeignToplevelHandleV1, +) -> String +{ + let Some( info ) = list.info( handle ) else { return String::new(); }; + if !info.app_id.is_empty() { return info.app_id; } + if !info.title.is_empty() { return info.title; } + info.identifier +} + impl ForeignToplevelListHandler for AppData { fn foreign_toplevel_list_state( &mut self ) -> &mut ForeignToplevelList @@ -427,9 +444,7 @@ impl ForeignToplevelListHandler for AppData ) { let id = handle.id().protocol_id(); - let app_id = self.foreign_toplevel_list.info( &handle ) - .map( |i| i.app_id ) - .unwrap_or_default(); + let app_id = toplevel_display_id( &self.foreign_toplevel_list, &handle ); if let Some( msg ) = self.app.on_toplevel_event( ToplevelEvent::Opened { id, app_id } ) { self.pending_msgs.push( msg ); @@ -451,9 +466,7 @@ impl ForeignToplevelListHandler for AppData // but the app_id-can-change-mid-life case still produces a // correct sequence (old refcount drops, new one bumps). let id = handle.id().protocol_id(); - let app_id = self.foreign_toplevel_list.info( &handle ) - .map( |i| i.app_id ) - .unwrap_or_default(); + let app_id = toplevel_display_id( &self.foreign_toplevel_list, &handle ); if let Some( msg ) = self.app.on_toplevel_event( ToplevelEvent::Opened { id, app_id } ) { self.pending_msgs.push( msg ); diff --git a/src/event_loop/mod.rs b/src/event_loop/mod.rs index 2ccea1c..fcd5d1b 100644 --- a/src/event_loop/mod.rs +++ b/src/event_loop/mod.rs @@ -304,6 +304,8 @@ pub( crate ) fn try_run( app: A ) -> Result<(), RunError> debug_layout, pending_msgs: Vec::new(), pending_drag_inits: Vec::new(), + tooltip_pending: None, + tooltip_visible: None, qh: qh.clone(), last_pointer_serial: 0, last_input_serial: 0, @@ -374,7 +376,11 @@ pub( crate ) fn try_run( app: A ) -> Result<(), RunError> // at a fixed rate. The one exception is a pending long-press: // cap the wait at its deadline so a perfectly still press still // fires on time. - let timeout = data.next_long_press_wakeup(); + let timeout = match ( data.next_long_press_wakeup(), data.next_tooltip_wakeup() ) + { + ( Some( a ), Some( b ) ) => Some( a.min( b ) ), + ( a, b ) => a.or( b ), + }; match event_loop.dispatch( timeout, &mut data ) { Ok( () ) => {}, @@ -404,6 +410,7 @@ pub( crate ) fn try_run( app: A ) -> Result<(), RunError> // emits its stored message and flips into drag mode for the rest // of the gesture. data.check_long_press_deadlines(); + data.check_tooltip_deadline(); // Poll external messages (immediate async results, e.g. PAM auth channel) let ext: Vec<_> = data.app.poll_external(); @@ -491,7 +498,9 @@ pub( crate ) fn try_run( app: A ) -> Result<(), RunError> } if data.overlays_dirty { - data.cached_overlays = Some( data.app.overlays() ); + let mut specs = data.app.overlays(); + if let Some( ts ) = data.tooltip_overlay() { specs.push( ts ); } + data.cached_overlays = Some( specs ); data.overlays_dirty = false; } draw_frame( &mut data ); @@ -615,7 +624,8 @@ pub fn diff_overlay_ids( /// [`SurfaceKind::Pending`] and the `new_output` handler will bring them up. fn reconcile_overlays( data: &mut AppData ) { - let specs = data.app.overlays(); + let mut specs = data.app.overlays(); + if let Some( ts ) = data.tooltip_overlay() { specs.push( ts ); } let next_ids: Vec<_> = specs.iter().map( |s| s.id ).collect(); let wanted: HashSet = next_ids.iter().copied().collect(); @@ -863,6 +873,7 @@ fn reconcile_overlays( data: &mut AppData ) } let mut ss = SurfaceState::::new( surface, 0.0, String::new() ); ss.last_requested_size = spec.size; + ss.layer_anchor = Some( spec.anchor ); overlays_m.insert( spec.id, ss ); } } diff --git a/src/input/dispatch.rs b/src/input/dispatch.rs index 651bd6c..cc740be 100644 --- a/src/input/dispatch.rs +++ b/src/input/dispatch.rs @@ -65,6 +65,26 @@ impl AppData impl AppData { + /// Top-left of `focus` in main-surface (global) coordinates. Used to + /// translate per-surface pointer coords into a single coordinate + /// space the app can reason about during drag-and-drop across + /// surfaces. + pub( crate ) fn surface_offset_for( &self, focus: SurfaceFocus ) -> ( f32, f32 ) + { + let SurfaceFocus::Overlay( id ) = focus else { return ( 0.0, 0.0 ); }; + let Some( ss ) = self.overlays.get( &id ) else { return ( 0.0, 0.0 ); }; + let Some( anchor ) = ss.layer_anchor else { return ( 0.0, 0.0 ); }; + let ( sw, sh ) = ( self.main.width as f32, self.main.height as f32 ); + let ( w, h ) = ( ss.width as f32, ss.height as f32 ); + let x = if anchor.left { 0.0 } + else if anchor.right { sw - w } + else { ( sw - w ) / 2.0 }; + let y = if anchor.top { 0.0 } + else if anchor.bottom { sh - h } + else { ( sh - h ) / 2.0 }; + ( x, y ) + } + /// Apply the outcome of a motion event. Non-blocking side-effects /// only: push messages, call app callbacks, set redraw flags. pub( super ) fn apply_move_outcome @@ -79,10 +99,15 @@ impl AppData MoveOutcome::Idle => {} MoveOutcome::Drag { pos } => { - self.app.on_drag_move( pos.x, pos.y ); + let ( ox, oy ) = self.surface_offset_for( focus ); + self.app.on_drag_move( pos.x + ox, pos.y + oy ); self.dirty_caches(); self.surface_mut( focus ).request_redraw(); self.main.request_redraw(); + for ss in self.overlays.values_mut() + { + ss.request_redraw(); + } } MoveOutcome::Slider { msg } => { @@ -141,7 +166,8 @@ impl AppData { ReleaseEvent::Drop { pos } => { - if let Some( msg ) = self.app.on_drop( pos.x, pos.y ) + let ( ox, oy ) = self.surface_offset_for( focus ); + if let Some( msg ) = self.app.on_drop( pos.x + ox, pos.y + oy ) { self.pending_msgs.push( msg ); } @@ -149,6 +175,10 @@ impl AppData self.dirty_caches(); self.main.request_redraw(); self.main.frame_pending = false; + for ss in self.overlays.values_mut() + { + ss.request_redraw(); + } } ReleaseEvent::SwipeLeft => { diff --git a/src/input/gesture.rs b/src/input/gesture.rs index 51cc3fd..6fe8e79 100644 --- a/src/input/gesture.rs +++ b/src/input/gesture.rs @@ -641,6 +641,7 @@ mod tests handlers: WidgetHandlers::Button { on_press, on_long_press, on_drag_start, on_escape: None, repeating: false }, keyboard_focusable: true, cursor: crate::types::CursorShape::Default, + tooltip: None, } } diff --git a/src/input/pointer.rs b/src/input/pointer.rs index 2068615..8af6b64 100644 --- a/src/input/pointer.rs +++ b/src/input/pointer.rs @@ -84,9 +84,16 @@ impl PointerHandler for AppData { let redraw = hover_affects_paint( &self.surface( focus ).widget_rects, old_hover ) || hover_affects_paint( &self.surface( focus ).widget_rects, new_hover ); - let ss = self.surface_mut( focus ); - ss.hovered_idx = new_hover; - if redraw { ss.needs_redraw = true; } + { + let ss = self.surface_mut( focus ); + ss.hovered_idx = new_hover; + if redraw { ss.needs_redraw = true; } + } + match new_hover + { + Some( idx ) => self.arm_tooltip( focus, idx ), + None => self.cancel_tooltip(), + } } // Mouse drag-promotion: a left-button press whose hit @@ -137,7 +144,8 @@ impl PointerHandler for AppData ( m, o ) }; self.app.update( ds_msg ); - self.app.on_drag_move( origin.x, origin.y ); + let ( ox, oy ) = self.surface_offset_for( focus ); + self.app.on_drag_move( origin.x + ox, origin.y + oy ); self.dirty_caches(); self.stop_button_repeat(); } @@ -228,6 +236,7 @@ impl PointerHandler for AppData { self.last_pointer_serial = serial; self.last_input_serial = serial; + self.cancel_tooltip(); let pos = self.surface( focus ).to_physical( event.position.0, event.position.1 ); self.pointer_pos = pos; self.app.on_pointer_move( pos.x, pos.y ); diff --git a/src/theme/mod.rs b/src/theme/mod.rs index 0995255..abe6e38 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -43,7 +43,17 @@ use std::collections::HashMap; use std::path::{ Path, PathBuf }; -use std::sync::{ Arc, Mutex, RwLock }; +use std::sync::{ Arc, Mutex, OnceLock, RwLock }; + +fn system_fontdb() -> Arc +{ + static DB: OnceLock> = OnceLock::new(); + DB.get_or_init( || { + let mut db = resvg::usvg::fontdb::Database::new(); + db.load_system_fonts(); + Arc::new( db ) + } ).clone() +} use serde::{ Deserialize, Serialize }; @@ -897,6 +907,7 @@ pub fn decode_svg_bytes( svg_bytes: &[u8], size: u32 ) -> Option<( Arc>, { opts.default_size = ds; } + opts.fontdb = system_fontdb(); let tree = resvg::usvg::Tree::from_data( svg_bytes, &opts ).ok()?; let svg_size = tree.size(); let longest = svg_size.width().max( svg_size.height() ); diff --git a/src/widget/button.rs b/src/widget/button.rs index 15b91a1..ba349a1 100644 --- a/src/widget/button.rs +++ b/src/widget/button.rs @@ -128,6 +128,7 @@ pub struct Button /// cancel, and on long-press promotion. Default `false` — most /// buttons fire on tap only. pub repeating: bool, + pub tooltip: Option, } impl Button @@ -147,9 +148,17 @@ impl Button focusable: true, cursor: None, repeating: false, + tooltip: None, } } + /// Hint shown after a 600 ms pointer dwell. Pointer-only. + pub fn tooltip( mut self, text: impl Into ) -> Self + { + self.tooltip = Some( text.into() ); + self + } + /// Override the pointer cursor shape shown on hover. pub fn cursor( mut self, shape: crate::types::CursorShape ) -> Self { @@ -174,6 +183,7 @@ impl Button focusable: true, cursor: None, repeating: false, + tooltip: None, } } @@ -484,6 +494,7 @@ impl Button focusable: self.focusable, cursor: self.cursor, repeating: self.repeating, + tooltip: self.tooltip, } } } @@ -528,6 +539,45 @@ mod tests assert!( b.on_press.is_none() ); } + #[ test ] + fn tooltip_none_by_default() + { + let b = Button::<()>::new( "ok".into() ); + assert!( b.tooltip.is_none() ); + let b = Button::<()>::new_icon( Arc::new( vec![] ), 0, 0 ); + assert!( b.tooltip.is_none() ); + } + + #[ test ] + fn tooltip_builder_sets_text() + { + let b = Button::<()>::new( "ok".into() ).tooltip( "Save document" ); + assert_eq!( b.tooltip.as_deref(), Some( "Save document" ) ); + } + + #[ test ] + fn tooltip_survives_map_msg() + { + let b = Button::<()>::new( "ok".into() ).tooltip( "Hint" ); + let f: super::super::MapFn<(), u32> = std::sync::Arc::new( |()| 0u32 ); + let mapped = b.map_msg( &f ); + assert_eq!( mapped.tooltip.as_deref(), Some( "Hint" ) ); + } + + #[ test ] + fn element_tooltip_returns_button_tooltip() + { + let e: super::super::Element<()> = Button::<()>::new( "ok".into() ).tooltip( "Hi" ).into_element(); + assert_eq!( e.tooltip(), Some( "Hi" ) ); + } + + #[ test ] + fn element_tooltip_none_for_non_button_widgets() + { + let t: super::super::Element<()> = super::super::text( "label" ).into(); + assert!( t.tooltip().is_none() ); + } + #[ test ] fn on_press_maybe_none_leaves_disabled() { diff --git a/src/widget/mod.rs b/src/widget/mod.rs index b7ef607..b588104 100755 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -423,6 +423,7 @@ pub struct LaidOutWidget /// dispatch can pick the right shape without re-walking the /// element tree. pub cursor: crate::types::CursorShape, + pub tooltip: Option, } impl Clone for LaidOutWidget @@ -438,6 +439,7 @@ impl Clone for LaidOutWidget handlers: self.handlers.clone(), keyboard_focusable: self.keyboard_focusable, cursor: self.cursor, + tooltip: self.tooltip.clone(), } } } @@ -664,6 +666,16 @@ impl Element } } + /// Add an arm here to opt a widget kind into the auto-tooltip flow. + pub fn tooltip( &self ) -> Option<&str> + { + match self + { + Element::Button( b ) => b.tooltip.as_deref(), + _ => None, + } + } + /// Snapshot the widget's callbacks/value into a [`WidgetHandlers`] for /// O(1) dispatch later. Called once per focusable leaf during the layout /// pass; the handlers are then stored alongside the rect in diff --git a/themes/default/theme.json b/themes/default/theme.json index 1442a7c..5c3c59a 100644 --- a/themes/default/theme.json +++ b/themes/default/theme.json @@ -217,14 +217,14 @@ "focus_ring": "@teal" }, "slots": { - "bg-page": { "type": "color", "value": "@navy", "meta": { "semantic": "palette/bg" } }, + "bg-page": { "type": "color", "value": "@navy", "meta": { "semantic": "palette/bg" } }, "surface": { "type": "color", "value": "@midnight/EE", "meta": { "semantic": "palette/surface" } }, - "surface-alt": { "type": "color", "value": "@indigo/D9", "meta": { "semantic": "palette/surface_alt" } }, - "text-primary": { "type": "color", "value": "@white", "meta": { "semantic": "palette/text_primary" } }, - "text-secondary": { "type": "color", "value": "@white/B3", "meta": { "semantic": "palette/text_secondary" } }, - "accent": { "type": "color", "value": "@cyan", "meta": { "semantic": "palette/accent" } }, - "divider": { "type": "color", "value": "@white/14", "meta": { "semantic": "palette/divider" } }, - "icon": { "type": "color", "value": "@white", "meta": { "semantic": "palette/icon" } }, + "surface-alt": { "type": "color", "value": "@white/D9", "meta": { "semantic": "palette/surface_alt" } }, + "text-primary": { "type": "color", "value": "@navy", "meta": { "semantic": "palette/text_primary" } }, + "text-secondary": { "type": "color", "value": "@white/B3", "meta": { "semantic": "palette/text_secondary" } }, + "accent": { "type": "color", "value": "@cyan", "meta": { "semantic": "palette/accent" } }, + "divider": { "type": "color", "value": "@white/14", "meta": { "semantic": "palette/divider" } }, + "icon": { "type": "color", "value": "@white", "meta": { "semantic": "palette/icon" } }, "shadows-glass": { "type": "shadows",