A horizontal swipe commits on a fling; App::overlay_namespace; flat Quick Settings slots in the default theme; the on-screen keyboard's rows twice as far apart, with presses between keys kept; LTK_PERF_RASTER
CI / build + test (push) Canceled after 0s
CI / cargo audit (push) Canceled after 0s

Flings. A horizontal swipe committed only past `swipe_horizontal_threshold` of the surface width, so a quick flick that stopped short, the usual way to turn a page on a phone, snapped back. The new `input/gesture/fling.rs` holds a `FlingTracker` that measures the finger's speed along one axis over the last 100 ms from the events' own timestamps, so a finger that rested before lifting reads as still. `GestureState` keeps one for the x axis, `fling_x`, cleared on every press; the touch handler feeds it on down, motion and up with the `wl_touch` time, which it used to ignore. On release a horizontal swipe also commits when the finger leaves at 500 logical px/s or more in the direction of travel after at least 24 px, and does not commit when it leaves that fast the other way, whatever the distance; both thresholds scale with the surface. Pointer drags and the vertical swipes keep the distance rule, and the gesture tests, which never feed the tracker, are unchanged. Two tests cover the speed and a rest before lifting.
`App::overlay_namespace`. Every layer overlay was created with the namespace `ltk-overlay`, so a compositor could not tell one panel from another. The new method returns the namespace of overlay `id`, read when its surface is created, and `None`, the default, keeps `ltk-overlay`. It follows `overlay_title` and `overlay_opaque_region` and breaks no app. Crustace names its Quick Settings and launcher overlays with it, and forge draws a blurred backdrop behind them.
Flat Quick Settings slots. The default theme gains `surface-qs-tile`, `surface-qs-tile-active`, `surface-qs-slider-track` and `surface-qs-slider-fill` in both modes: a solid translucent fill, white or navy for the tiles and tracks and cyan for the active tile and the fill, with no shadows, no inset shadows and no gradients. The glass slots the panel used put a blurred shadow, one to five inset shadows, some with blend modes, and a gradient under each of its nineteen surfaces, all rasterised on every frame of its slide. The shared slots are untouched, so the dock and other applications look as before.
The on-screen keyboard. The rows are now `ROW_GAP`, twice `GAP`, apart, 12 logical px against the 6 between keys; `height` counts it, so the panel grows by 18 to 24 px depending on its rows and a host that reserves its height follows. A press between keys, which the wider gap would have made more common, no longer goes nowhere: `key_at` gives it to the nearest key within one row gap.
`LTK_PERF_RASTER`. With it set, every GLES raster, full, partial or subsurface, logs its surface size, the time to submit and, after a `glFinish`, the time until the GPU finished. The `glFinish` stalls the pipeline, so it is for measuring only; it gave the cost of the old Quick Settings panel, 27 to 40 ms a raster on the Librem 5 with peaks of 57 to 143 ms.
This commit is contained in:
2026-09-30 10:17:22 +02:00
parent a0ceef51ef
commit 88f69ebffe
10 changed files with 218 additions and 14 deletions
+5
View File
@@ -561,6 +561,11 @@ pub trait App: 'static
/// in the compositor's window lists. Default: the [`app_id`](Self::app_id). /// in the compositor's window lists. Default: the [`app_id`](Self::app_id).
fn overlay_title( &self, _id: OverlayId ) -> Option<String> { None } fn overlay_title( &self, _id: OverlayId ) -> Option<String> { None }
/// wlr-layer-shell namespace of a layer overlay, so the compositor can
/// tell it apart from the rest. Read when the surface is created.
/// Default: `"ltk-overlay"`.
fn overlay_namespace( &self, _id: OverlayId ) -> Option<&'static str> { None }
/// Describe the input-transparent child surfaces composited over the main /// Describe the input-transparent child surfaces composited over the main
/// surface this frame. Each becomes a `wl_subsurface` the compositor moves /// surface this frame. Each becomes a `wl_subsurface` the compositor moves
/// by position; see [`SubsurfaceSpec`]. Diffed across frames by /// by position; see [`SubsurfaceSpec`]. Diffed across frames by
+5
View File
@@ -25,6 +25,7 @@ use smithay_client_toolkit::compositor::CompositorState;
use smithay_client_toolkit::reexports::client::protocol::wl_surface::WlSurface; use smithay_client_toolkit::reexports::client::protocol::wl_surface::WlSurface;
use crate::egl_context::EglContext; use crate::egl_context::EglContext;
use crate::event_loop::perf::RasterTrace;
use crate::event_loop::SurfaceState; use crate::event_loop::SurfaceState;
use crate::render::Canvas; use crate::render::Canvas;
use crate::types::{ Color, Rect }; use crate::types::{ Color, Rect };
@@ -61,6 +62,7 @@ pub( crate ) fn draw_surface_full_gpu<Msg: Clone>(
{ {
let Some( es ) = ss.egl_surface.as_ref() else { return }; let Some( es ) = ss.egl_surface.as_ref() else { return };
if egl_ctx.make_current( es ).is_err() { return; } if egl_ctx.make_current( es ).is_err() { return; }
let trace = RasterTrace::begin();
let canvas = ss.canvas.get_or_insert_with( || let canvas = ss.canvas.get_or_insert_with( ||
{ {
@@ -112,6 +114,7 @@ pub( crate ) fn draw_surface_full_gpu<Msg: Clone>(
draw_fallback_banner( canvas, pw, sf ); draw_fallback_banner( canvas, pw, sf );
canvas.present(); canvas.present();
if let Some( t ) = trace { t.end( egl_ctx.gl(), "full", pw, ph ); }
commit_draw_ctx( &mut ss.frame, ss.focused_idx, ss.hovered_idx, ss.gesture.pressed_idx, ctx ); commit_draw_ctx( &mut ss.frame, ss.focused_idx, ss.hovered_idx, ss.gesture.pressed_idx, ctx );
ss.content_dirty = false; ss.content_dirty = false;
@@ -152,6 +155,7 @@ pub( crate ) fn draw_surface_partial_gpu<Msg: Clone>(
{ {
let Some( es ) = ss.egl_surface.as_ref() else { return }; let Some( es ) = ss.egl_surface.as_ref() else { return };
if egl_ctx.make_current( es ).is_err() { return; } if egl_ctx.make_current( es ).is_err() { return; }
let trace = RasterTrace::begin();
let canvas = ss.canvas.as_mut().expect( "partial path requires existing canvas" ); let canvas = ss.canvas.as_mut().expect( "partial path requires existing canvas" );
if canvas.size() != ( pw, ph ) if canvas.size() != ( pw, ph )
@@ -189,6 +193,7 @@ pub( crate ) fn draw_surface_partial_gpu<Msg: Clone>(
canvas.clear_clip(); canvas.clear_clip();
canvas.present(); canvas.present();
if let Some( t ) = trace { t.end( egl_ctx.gl(), "partial", pw, ph ); }
commit_draw_ctx( &mut ss.frame, ss.focused_idx, ss.hovered_idx, ss.gesture.pressed_idx, ctx ); commit_draw_ctx( &mut ss.frame, ss.focused_idx, ss.hovered_idx, ss.gesture.pressed_idx, ctx );
ss.content_dirty = false; ss.content_dirty = false;
+1 -1
View File
@@ -360,7 +360,7 @@ pub( super ) fn reconcile_overlays<A: App>( data: &mut AppData<A> )
anchor: spec.anchor, anchor: spec.anchor,
size: to_logical_size( resolved_size ), size: to_logical_size( resolved_size ),
keyboard_exclusive: spec.keyboard_exclusive, keyboard_exclusive: spec.keyboard_exclusive,
namespace: "ltk-overlay", namespace: app.overlay_namespace( spec.id ).unwrap_or( "ltk-overlay" ),
}; };
let mut surface = SurfaceKind::Pending( cfg ); let mut surface = SurfaceKind::Pending( cfg );
if let Some( ref output ) = output_opt if let Some( ref output ) = output_opt
+34
View File
@@ -37,6 +37,40 @@ fn perf_warn_enabled() -> bool
) )
} }
/// Wall time of one GLES raster, split at submission and at GPU completion.
/// Opt-in via `LTK_PERF_RASTER`; the `glFinish` it adds stalls the pipeline.
pub( crate ) struct RasterTrace
{
start: Instant,
}
impl RasterTrace
{
pub( crate ) fn begin() -> Option<Self>
{
static ENABLED: OnceLock<bool> = OnceLock::new();
let enabled = *ENABLED.get_or_init( ||
std::env::var( "LTK_PERF_RASTER" )
.map( |v| !v.is_empty() && v != "0" )
.unwrap_or( false )
);
enabled.then( || Self { start: Instant::now() } )
}
pub( crate ) fn end( self, gl: &glow::Context, label: &str, pw: u32, ph: u32 )
{
use glow::HasContext;
let submitted = self.start.elapsed();
unsafe { gl.finish(); }
let done = self.start.elapsed();
eprintln!(
"[ltk][raster] {label} {pw}x{ph} cpu={:.2}ms gpu_done={:.2}ms",
submitted.as_secs_f64() * 1000.0,
done.as_secs_f64() * 1000.0,
);
}
}
/// Per-runtime performance-guard state. Lives on `AppData`. /// Per-runtime performance-guard state. Lives on `AppData`.
pub struct PerfState pub struct PerfState
{ {
+2
View File
@@ -395,6 +395,7 @@ fn render_slot_gpu<Msg: Clone>(
} }
let es = slot.egl_surface.as_ref().unwrap(); let es = slot.egl_surface.as_ref().unwrap();
if egl_ctx.make_current( es ).is_err() { return; } if egl_ctx.make_current( es ).is_err() { return; }
let trace = super::perf::RasterTrace::begin();
// The canvas (and its compiled shader programs) is shared across all // The canvas (and its compiled shader programs) is shared across all
// subsurface rasters, so a lazily re-created panel doesn't recompile. // subsurface rasters, so a lazily re-created panel doesn't recompile.
@@ -420,6 +421,7 @@ fn render_slot_gpu<Msg: Clone>(
let mut ctx = slot_draw_ctx::<Msg>( &mut slot.scroll_canvases, &mut slot.viewport_canvases ); let mut ctx = slot_draw_ctx::<Msg>( &mut slot.scroll_canvases, &mut slot.viewport_canvases );
layout_and_draw::<Msg>( view, canvas, screen_rect, &mut ctx, 0 ); layout_and_draw::<Msg>( view, canvas, screen_rect, &mut ctx, 0 );
canvas.present(); canvas.present();
if let Some( t ) = trace { t.end( egl_ctx.gl(), "subsurface", pw, ph ); }
slot.scroll_canvases = ctx.scroll_canvases; slot.scroll_canvases = ctx.scroll_canvases;
slot.viewport_canvases = ctx.viewport_canvases; slot.viewport_canvases = ctx.viewport_canvases;
+77
View File
@@ -0,0 +1,77 @@
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
//! Release velocity of a swipe, so a quick flick commits it whatever its
//! travel and a quick flick back cancels it.
use std::collections::VecDeque;
/// Release speed, logical px/s, that decides a swipe on its own.
pub const FLING_VELOCITY: f32 = 500.0;
/// Travel a forward fling needs, logical px, so a twitch does not commit.
pub const FLING_MIN_TRAVEL: f32 = 24.0;
/// Speed of a finger along one axis over the last `WINDOW_MS` of input,
/// from the events' own timestamps. A finger that rested before lifting
/// has no samples left in the window and reads as still.
#[ derive( Debug, Clone, Default ) ]
pub struct FlingTracker
{
samples: VecDeque<( u32, f32 )>,
}
impl FlingTracker
{
const WINDOW_MS: u32 = 100;
pub fn clear( &mut self ) { self.samples.clear(); }
pub fn push( &mut self, time_ms: u32, pos: f32 )
{
self.samples.push_back( ( time_ms, pos ) );
while let Some( &( t, _ ) ) = self.samples.front()
{
if time_ms.wrapping_sub( t ) <= Self::WINDOW_MS { break; }
self.samples.pop_front();
}
}
/// Px/s in the unit of the pushed positions; 0 when the window spans
/// too little time to tell.
pub fn velocity( &self ) -> f32
{
let ( Some( &( t0, p0 ) ), Some( &( t1, p1 ) ) ) = ( self.samples.front(), self.samples.back() ) else
{
return 0.0;
};
let dt = t1.wrapping_sub( t0 );
if dt < 8 { return 0.0; }
( p1 - p0 ) * 1000.0 / dt as f32
}
}
#[ cfg( test ) ]
mod tests
{
use super::*;
#[ test ]
fn measures_recent_speed()
{
let mut t = FlingTracker::default();
t.push( 0, 0.0 );
t.push( 20, 10.0 );
t.push( 40, 20.0 );
assert!( ( t.velocity() - 500.0 ).abs() < 1e-3 );
}
#[ test ]
fn forgets_a_rest_before_lifting()
{
let mut t = FlingTracker::default();
t.push( 0, 0.0 );
t.push( 20, 40.0 );
t.push( 300, 40.0 );
assert_eq!( t.velocity(), 0.0 );
}
}
+13 -1
View File
@@ -38,8 +38,10 @@ use crate::tree::{ find_handlers, find_widget, find_widget_at };
use crate::types::{ Point, Rect }; use crate::types::{ Point, Rect };
use crate::widget::LaidOutWidget; use crate::widget::LaidOutWidget;
mod fling;
mod outcomes; mod outcomes;
pub( crate ) use fling::FlingTracker;
pub( crate ) use outcomes::*; pub( crate ) use outcomes::*;
#[ cfg( test ) ] #[ cfg( test ) ]
@@ -147,6 +149,8 @@ pub struct GestureState<Msg: Clone>
/// Pattern-grid node the finger was last over, so `on_node` fires /// Pattern-grid node the finger was last over, so `on_node` fires
/// once per node rather than once per motion event. /// once per node rather than once per motion event.
pub last_pattern_node: Option<u8>, pub last_pattern_node: Option<u8>,
/// Horizontal release velocity, fed by the touch handler with event times.
pub fling_x: FlingTracker,
} }
impl<Msg: Clone> Default for GestureState<Msg> impl<Msg: Clone> Default for GestureState<Msg>
@@ -180,6 +184,7 @@ impl<Msg: Clone> GestureState<Msg>
overscroll_y: 0.0, overscroll_y: 0.0,
mouse_press: false, mouse_press: false,
last_pattern_node: None, last_pattern_node: None,
fling_x: FlingTracker::default(),
} }
} }
@@ -231,6 +236,7 @@ impl<Msg: Clone> GestureState<Msg>
self.horizontal_drag_started = false; self.horizontal_drag_started = false;
self.vertical_drag_started = false; self.vertical_drag_started = false;
self.swipe_axis = None; self.swipe_axis = None;
self.fling_x.clear();
self.pressed_idx = hit; self.pressed_idx = hit;
// A fresh press resets any stale long-press / source state // A fresh press resets any stale long-press / source state
// from a prior gesture that never released cleanly. The // from a prior gesture that never released cleanly. The
@@ -596,8 +602,14 @@ impl<Msg: Clone> GestureState<Msg>
{ {
let dx = pos.x - start.x; let dx = pos.x - start.x;
let width = swipe.surface_width; let width = swipe.surface_width;
let scale = swipe.scale.max( 1.0 );
let v = self.fling_x.velocity();
let fast = v.abs() >= fling::FLING_VELOCITY * scale;
let flung = fast && v.signum() == dx.signum() && dx.abs() >= fling::FLING_MIN_TRAVEL * scale;
let flung_back = fast && v.signum() != dx.signum();
let committed = width > 0 let committed = width > 0
&& dx.abs() >= width as f32 * swipe.horizontal_thresh; && !flung_back
&& ( flung || dx.abs() >= width as f32 * swipe.horizontal_thresh );
if committed if committed
{ {
self.start = None; self.start = None;
+7 -3
View File
@@ -53,7 +53,7 @@ impl<A: App> TouchHandler for AppData<A>
qh: &QueueHandle<Self>, qh: &QueueHandle<Self>,
_touch: &WlTouch, _touch: &WlTouch,
serial: u32, serial: u32,
_time: u32, time: u32,
surface: WlSurface, surface: WlSurface,
id: i32, id: i32,
position: ( f64, f64 ), position: ( f64, f64 ),
@@ -157,6 +157,7 @@ impl<A: App> TouchHandler for AppData<A>
{ {
let ss = self.surface_mut( focus ); let ss = self.surface_mut( focus );
let result = ss.gesture.on_press( pos, &ss.frame.widget_rects, &ss.frame.scroll_rects ); let result = ss.gesture.on_press( pos, &ss.frame.widget_rects, &ss.frame.scroll_rects );
ss.gesture.fling_x.push( time, pos.x );
ss.needs_redraw = true; ss.needs_redraw = true;
result result
}; };
@@ -222,7 +223,7 @@ impl<A: App> TouchHandler for AppData<A>
_qh: &QueueHandle<Self>, _qh: &QueueHandle<Self>,
_touch: &WlTouch, _touch: &WlTouch,
_serial: u32, _serial: u32,
_time: u32, time: u32,
id: i32, id: i32,
) )
{ {
@@ -257,6 +258,8 @@ impl<A: App> TouchHandler for AppData<A>
return; return;
} }
let x = self.pointer_pos.x;
self.surface_mut( focus ).gesture.fling_x.push( time, x );
self.release_primary( focus ); self.release_primary( focus );
} }
@@ -265,7 +268,7 @@ impl<A: App> TouchHandler for AppData<A>
_conn: &Connection, _conn: &Connection,
_qh: &QueueHandle<Self>, _qh: &QueueHandle<Self>,
_touch: &WlTouch, _touch: &WlTouch,
_time: u32, time: u32,
id: i32, id: i32,
position: ( f64, f64 ), position: ( f64, f64 ),
) )
@@ -308,6 +311,7 @@ impl<A: App> TouchHandler for AppData<A>
let outcome = let outcome =
{ {
let ss = self.surface_mut( focus ); let ss = self.surface_mut( focus );
ss.gesture.fling_x.push( time, pp.x );
ss.gesture.on_move( pp, &ss.frame.widget_rects, &mut ss.frame.scroll_offsets, &swipe, global_drag ) ss.gesture.on_move( pp, &ss.frame.widget_rects, &mut ss.frame.scroll_offsets, &swipe, global_drag )
}; };
self.apply_move_outcome( focus, outcome ); self.apply_move_outcome( focus, outcome );
+21 -4
View File
@@ -22,6 +22,7 @@ const REPEAT_INTERVAL: Duration = Duration::from_millis( 60 );
const SHIFT_DOUBLE_TAP: Duration = Duration::from_millis( 350 ); const SHIFT_DOUBLE_TAP: Duration = Duration::from_millis( 350 );
/// Design sizes in logical pixels; the panel multiplies them by its scale. /// Design sizes in logical pixels; the panel multiplies them by its scale.
const GAP: f32 = 6.0; const GAP: f32 = 6.0;
const ROW_GAP: f32 = 2.0 * GAP;
const PADDING: f32 = 6.0; const PADDING: f32 = 6.0;
/// What the field being typed into is for; picks the extra row above the layout. /// What the field being typed into is for; picks the extra row above the layout.
@@ -224,7 +225,7 @@ impl Panel
pub fn height( &self, width: f32, scale: f32 ) -> f32 pub fn height( &self, width: f32, scale: f32 ) -> f32
{ {
let rows = ( self.bar_rows().len() + self.layout().rows( &self.layer ).len() ) as f32; let rows = ( self.bar_rows().len() + self.layout().rows( &self.layer ).len() ) as f32;
rows * self.key_unit( width, scale ) + ( ( rows - 1.0 ).max( 0.0 ) * GAP + 2.0 * PADDING ) * scale rows * self.key_unit( width, scale ) + ( ( rows - 1.0 ).max( 0.0 ) * ROW_GAP + 2.0 * PADDING ) * scale
} }
/// Width of one key unit of the layout's rows, `width` pixels wide: the /// Width of one key unit of the layout's rows, `width` pixels wide: the
@@ -247,6 +248,7 @@ impl Panel
self.scale = scale; self.scale = scale;
self.keys.clear(); self.keys.clear();
let gap = GAP * scale; let gap = GAP * scale;
let row_gap = ROW_GAP * scale;
let pad = PADDING * scale; let pad = PADDING * scale;
let key_h = self.key_unit( rect.width, scale ); let key_h = self.key_unit( rect.width, scale );
let avail = rect.width - 2.0 * pad; let avail = rect.width - 2.0 * pad;
@@ -258,7 +260,7 @@ impl Panel
let units: f32 = row.iter().map( |k| k.width ).sum::<f32>().max( 1.0 ); let units: f32 = row.iter().map( |k| k.width ).sum::<f32>().max( 1.0 );
let unit = ( avail - gap * row.len().saturating_sub( 1 ) as f32 ) / units; let unit = ( avail - gap * row.len().saturating_sub( 1 ) as f32 ) / units;
self.push_row( row, rect.x + pad, y, unit, key_h, gap ); self.push_row( row, rect.x + pad, y, unit, key_h, gap );
y += key_h + gap; y += key_h + row_gap;
} }
let rows: Vec<Vec<Key>> = self.layout().rows( &self.layer ).iter() let rows: Vec<Vec<Key>> = self.layout().rows( &self.layer ).iter()
@@ -270,7 +272,7 @@ impl Panel
let units: f32 = row.iter().map( |k| k.width ).sum(); let units: f32 = row.iter().map( |k| k.width ).sum();
let row_w = units * unit + ( row.len().saturating_sub( 1 ) ) as f32 * gap; let row_w = units * unit + ( row.len().saturating_sub( 1 ) ) as f32 * gap;
self.push_row( row, rect.x + pad + ( avail - row_w ) / 2.0, y, unit, key_h, gap ); self.push_row( row, rect.x + pad + ( avail - row_w ) / 2.0, y, unit, key_h, gap );
y += key_h + gap; y += key_h + row_gap;
} }
self.popup = None; self.popup = None;
} }
@@ -297,7 +299,22 @@ impl Panel
fn key_at( &self, pos: Point ) -> Option<usize> fn key_at( &self, pos: Point ) -> Option<usize>
{ {
self.keys.iter().position( |k| k.rect.contains( pos ) ) if let Some( idx ) = self.keys.iter().position( |k| k.rect.contains( pos ) )
{
return Some( idx );
}
let reach = ROW_GAP * self.scale;
self.keys.iter()
.enumerate()
.map( |( i, k )|
{
let dx = ( k.rect.x - pos.x ).max( pos.x - ( k.rect.x + k.rect.width ) ).max( 0.0 );
let dy = ( k.rect.y - pos.y ).max( pos.y - ( k.rect.y + k.rect.height ) ).max( 0.0 );
( i, dx.hypot( dy ) )
} )
.filter( |&( _, d )| d <= reach )
.min_by( |a, b| a.1.total_cmp( &b.1 ) )
.map( |( i, _ )| i )
} }
/// A press at `pos`; `None` when it is outside the panel and the host should treat it as its own. /// A press at `pos`; `None` when it is outside the panel and the host should treat it as its own.
+48
View File
@@ -184,6 +184,30 @@
"meta": { "semantic": "effect/glass/panel" } "meta": { "semantic": "effect/glass/panel" }
}, },
"surface-qs-tile": {
"type": "surface",
"fill": { "type": "solid", "color": "@white/B3" },
"meta": { "semantic": "effect/qs/tile" }
},
"surface-qs-tile-active": {
"type": "surface",
"fill": { "type": "solid", "color": "@cyan" },
"meta": { "semantic": "effect/qs/tile-active" }
},
"surface-qs-slider-track": {
"type": "surface",
"fill": { "type": "solid", "color": "@white/80" },
"meta": { "semantic": "effect/qs/slider-track" }
},
"surface-qs-slider-fill": {
"type": "surface",
"fill": { "type": "solid", "color": "@cyan" },
"meta": { "semantic": "effect/qs/slider-fill" }
},
"surface-card-flat": { "surface-card-flat": {
"type": "surface", "type": "surface",
"fill": { "fill": {
@@ -305,6 +329,30 @@
"meta": { "semantic": "effect/glass/panel" } "meta": { "semantic": "effect/glass/panel" }
}, },
"surface-qs-tile": {
"type": "surface",
"fill": { "type": "solid", "color": "@navy/B3" },
"meta": { "semantic": "effect/qs/tile" }
},
"surface-qs-tile-active": {
"type": "surface",
"fill": { "type": "solid", "color": "@cyan" },
"meta": { "semantic": "effect/qs/tile-active" }
},
"surface-qs-slider-track": {
"type": "surface",
"fill": { "type": "solid", "color": "@navy/80" },
"meta": { "semantic": "effect/qs/slider-track" }
},
"surface-qs-slider-fill": {
"type": "surface",
"fill": { "type": "solid", "color": "@cyan" },
"meta": { "semantic": "effect/qs/slider-fill" }
},
"surface-card-flat": { "surface-card-flat": {
"type": "surface", "type": "surface",
"fill": "@surface-glass-dark", "fill": "@surface-glass-dark",