event_loop, slider, list_item: overlay exclusive zones follow the surface, on_release for sliders, list labels elide against the trailing slot
Exclusive zones (event_loop/overlays_reconcile.rs, event_loop/surface.rs). `OverlaySpec::size` is documented as physical pixels and converted to logical for `layer_surface.set_size` by dividing by the parent's integer scale; `exclusive_zone` sat right beside it in the same `LayerConfig` and was passed through raw, so at scale 2 the reserved band was expressed in a unit twice as coarse as the surface it is meant to match. Worse, `set_exclusive_zone` appeared exactly once in the whole crate — at materialize time — while the reconcile loop propagated later size changes through `last_requested_size`, so an overlay whose size kept being recomputed carried a reservation frozen at whatever the first frame produced. Crustace's dock is the visible case: it derives both numbers from the same `desktop_pill_height`, and with the zone stuck at the density-1 value (icon at its 40 px floor, 40 × 1.70 = 68 logical) while the surface grew to 87, a maximized window overlapped the top fifth of the dock — measured on a 1.75 output as 151 physical px of painted dock against a 120 px reserved band. The zone now goes through the same divisor as the size, with `-1` (ignore other zones) and `0` (reserve nothing) passing through untouched as the sentinels they are, and `SurfaceState` tracks `last_requested_zone` so the reconcile loop re-sends it whenever the spec moves, committing once for both. Slider::on_release / VSlider::on_release (widget/slider, widget/vslider, widget/handlers.rs, widget/element.rs, input/gesture). Sliders only had `on_change`, which fires on every motion event, so an app whose commit is expensive — a subprocess, a D-Bus round trip, a compositor reconfigure — paid for it per pixel of travel: dragging the text-scale slider in Eydos Settings wrote `gsettings` once per motion and swept the whole desktop through a repaint each time. The new builder fires once with the final value when the drag ends, leaving `on_change` to move the thumb and nothing else; `on_change` alone behaves exactly as before, and the mapped variant propagates through `map_msg` like its sibling. The handler snapshot carries the callback next to `on_change` and exposes `slider_release_msg`; the gesture machine's slider branch of `on_release`, which previously returned an empty event list, now resolves the widget through `find_widget`, recomputes the value from the release position with the same `slider_value_from_pos` the drag path uses, and pushes `ReleaseEvent::PushMsg` — the variant whose documentation already described "button press or final slider value on release". A unit test covers the new emission; the existing one asserting an empty release still holds, because it passes an empty widget list and the lookup finds nothing. ListItem elision (widget/list_item/mod.rs). The label and subtitle were painted with `draw_text` and no width budget, so a row title longer than its width ran under the trailing text or the disclosure icon instead of truncating. The trailing slots are now measured and positioned before the text is painted — their extent is what decides how much room the label has — and both lines are elided with an ellipsis against the space left over, accumulating per-character widths the way `Text` already did, with one icon gap kept between the text and whatever follows it. `Text` keeps its own inline copy of that algorithm for now; folding the two into a single crate-internal helper is the natural follow-up, and a precondition for teaching `Button` to elide once `Row` learns to distribute a width deficit instead of only leftover space.
This commit is contained in:
@@ -6,6 +6,7 @@ All notable changes to `ltk` are documented here. The format is based on [Keep a
|
||||
|
||||
### Added
|
||||
|
||||
- **`Slider::on_release` / `VSlider::on_release`** — fired once with the final value when the drag ends, so an app can keep an expensive commit (a subprocess, a D-Bus round trip, a compositor reconfigure) off the per-motion `on_change` path and still move the thumb live. The gesture machine emits it from the slider branch of `on_release`; `on_change` alone behaves exactly as before.
|
||||
- **`Viewport::local_viewport()`** — resolve the child's viewport-relative (`vw` / `vh` / `vmin`) and fluid `Length`s against the viewport's own rect instead of the root layout viewport the sub-canvas inherits. For fixed-size floating mini-UIs (a phone-shaped panel pinned to a corner of a desktop-wide surface) whose content is calibrated against the panel rect; scroll-like clips should keep the default inheritance.
|
||||
- **`ListItem::height( impl Into<Length> )` / `ListItem::font_size( impl Into<Length> )`** — override the theme row height (floored at the label's rendered height so text never clips) and the primary-label font size, mirroring the `Toggle` / `Radio` `height()` builders, so dense menus can trade the touch-target generosity for row density.
|
||||
- **Accessibility text scale** — `set_text_scale` / `text_scale` global multiplier (clamped `[0.5, 3.0]`) applied to every resolved font size (`Canvas::resolve_font` and the stock-widget `font_px` path); geometry is untouched. The run loop reads `org.gnome.desktop.interface text-scaling-factor` at startup and follows external changes via a `gsettings monitor` watcher thread, repainting on change — every ltk app tracks the desktop's "large text" setting live with no app-side wiring (silently fixed at 1.0 when `gsettings` is missing). Embedders driving `core::UiSurface` call `set_text_scale` themselves.
|
||||
@@ -41,6 +42,7 @@ All notable changes to `ltk` are documented here. The format is based on [Keep a
|
||||
|
||||
### Fixed
|
||||
|
||||
- **`ListItem` labels no longer run under the trailing slot.** The label and subtitle were painted with no width limit, so a title longer than the row overlapped the trailing text or the disclosure icon instead of truncating. The trailing slots are now laid out first and both lines are elided with an ellipsis against the space they leave, matching what `Text` already did.
|
||||
- **Text inputs no longer insert mid-string when the value grows after focus.** Focusing a text input pinned the cursor to a snapshot of `value.len()`; if the value kept growing without the widget seeing keystrokes (a field fed over IPC), the first normally-delivered key inserted at the stale position. The focus-time cursor is now an end-of-value sentinel that every consumer clamps to the current value length, collapsing to a concrete position on the first real keystroke or click.
|
||||
- **Single-line caret height now follows the text line.** The caret spanned `rect.height - 16`, so a field taller than its text line grew an oversized caret; it now measures `font_size + 4` and is vertically centered like the text, matching the multiline caret.
|
||||
- **Dialog action buttons no longer overflow the card on large surfaces.** The card's width cap was a fixed 480 px while the stock buttons inside grow fluidly with the surface, so on windows past the design size the right-aligned action row ran off the card's right edge. `Dialog::max_width` now takes `impl Into<Length>` (f32 call sites keep compiling as fixed px) and the default is `Length::fluid( 480.0 )`, matching the buttons' scaling curve.
|
||||
|
||||
@@ -269,6 +269,12 @@ slider( self.brightness )
|
||||
# }}
|
||||
```
|
||||
|
||||
`on_release( f )` fires once with the final value when the drag ends.
|
||||
Pair it with `on_change` — which keeps moving the thumb — when the commit
|
||||
is expensive (a subprocess, a D-Bus round trip, a compositor
|
||||
reconfigure): a sweep would otherwise pay for it on every motion event.
|
||||
`vslider` carries the same builder.
|
||||
|
||||
`accent_thumb( true )` swaps the default thumb for the two-circle
|
||||
brand-coloured variant. `track_surface( id )` and `fill_surface( id )`
|
||||
override the default theme slots.
|
||||
|
||||
@@ -129,6 +129,13 @@ pub( super ) fn reconcile_overlays<A: App>( data: &mut AppData<A> )
|
||||
( h as f32 / parent_scale ).round() as u32,
|
||||
)
|
||||
};
|
||||
// The exclusive zone measures the same band the surface occupies, so it
|
||||
// lives in the same space and needs the same conversion. `-1` (ignore
|
||||
// other zones) and `0` (reserve nothing) are sentinels, not distances.
|
||||
let to_logical_zone = move | z: i32 | -> i32
|
||||
{
|
||||
if z > 0 { ( z as f32 / parent_scale ).round() as i32 } else { z }
|
||||
};
|
||||
// Snapshot the previous-frame anchor lookup table so we can resolve
|
||||
// `anchor_widget_id` → `Rect` without holding a borrow on `data.main`
|
||||
// across the overlay-mut loop below.
|
||||
@@ -148,15 +155,24 @@ pub( super ) fn reconcile_overlays<A: App>( data: &mut AppData<A> )
|
||||
// sends a configure; the usual `on_configure` path picks up the
|
||||
// new dimensions and drives the redraw. Popups don't grow /
|
||||
// shrink mid-life — close and reopen instead.
|
||||
if resolved_size != ss.last_requested_size
|
||||
let resolved_zone = to_logical_zone( spec.exclusive_zone );
|
||||
if resolved_size != ss.last_requested_size || resolved_zone != ss.last_requested_zone
|
||||
{
|
||||
if let SurfaceKind::Layer( ref layer_surface ) = ss.surface
|
||||
{
|
||||
if resolved_size != ss.last_requested_size
|
||||
{
|
||||
let ( lw, lh ) = to_logical_size( resolved_size );
|
||||
layer_surface.set_size( lw, lh );
|
||||
layer_surface.commit();
|
||||
ss.last_requested_size = resolved_size;
|
||||
}
|
||||
if resolved_zone != ss.last_requested_zone
|
||||
{
|
||||
layer_surface.set_exclusive_zone( resolved_zone );
|
||||
ss.last_requested_zone = resolved_zone;
|
||||
}
|
||||
layer_surface.commit();
|
||||
}
|
||||
}
|
||||
// Compare the anchor at integer logical-pixel resolution:
|
||||
// floating-point jitter would otherwise call `reposition`
|
||||
@@ -301,7 +317,7 @@ pub( super ) fn reconcile_overlays<A: App>( data: &mut AppData<A> )
|
||||
let cfg = LayerConfig
|
||||
{
|
||||
layer: spec.layer.to_wlr_layer(),
|
||||
exclusive_zone: spec.exclusive_zone,
|
||||
exclusive_zone: to_logical_zone( spec.exclusive_zone ),
|
||||
anchor: spec.anchor,
|
||||
size: to_logical_size( resolved_size ),
|
||||
keyboard_exclusive: spec.keyboard_exclusive,
|
||||
@@ -319,6 +335,7 @@ pub( super ) fn reconcile_overlays<A: App>( data: &mut AppData<A> )
|
||||
// lands a frame or two later.
|
||||
ss.scale_factor = parent_scale_i;
|
||||
ss.last_requested_size = resolved_size;
|
||||
ss.last_requested_zone = to_logical_zone( spec.exclusive_zone );
|
||||
ss.layer_anchor = Some( spec.anchor );
|
||||
overlays_m.insert( spec.id, ss );
|
||||
}
|
||||
|
||||
@@ -276,6 +276,11 @@ pub( crate ) struct SurfaceState<Msg: Clone>
|
||||
/// 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 ),
|
||||
/// Logical exclusive zone last sent with `set_exclusive_zone`. Tracked
|
||||
/// alongside the size because a zone that stays at its creation-time
|
||||
/// value while the surface keeps resizing leaves the compositor
|
||||
/// reserving a band that no longer matches what the overlay paints.
|
||||
pub last_requested_zone: i32,
|
||||
pub layer_anchor: Option<crate::app::Anchor>,
|
||||
/// Anchor rect the xdg-popup positioner was last configured with.
|
||||
/// `None` for non-popup surfaces.
|
||||
@@ -321,6 +326,7 @@ impl<Msg: Clone> SurfaceState<Msg>
|
||||
titlebar_close_rect: Rect::default(),
|
||||
scale_factor: 1,
|
||||
last_requested_size: ( 0, 0 ),
|
||||
last_requested_zone: 0,
|
||||
layer_anchor: None,
|
||||
last_popup_anchor: None,
|
||||
popup_reposition_token: 0,
|
||||
|
||||
@@ -475,7 +475,7 @@ impl<Msg: Clone> GestureState<Msg>
|
||||
) -> Vec<ReleaseEvent<Msg>>
|
||||
{
|
||||
let pressed = self.pressed_idx.take();
|
||||
let was_dragging_slider = self.dragging_slider.is_some();
|
||||
let released_slider = self.dragging_slider;
|
||||
let long_press_fired = self.long_press_fired;
|
||||
let horizontal_drag_started = self.horizontal_drag_started;
|
||||
let vertical_drag_started = self.vertical_drag_started;
|
||||
@@ -504,11 +504,21 @@ impl<Msg: Clone> GestureState<Msg>
|
||||
return events;
|
||||
}
|
||||
|
||||
// Slider drag complete — not a swipe, not a tap.
|
||||
if was_dragging_slider
|
||||
// Slider drag complete — not a swipe, not a tap. The final value
|
||||
// goes out once here so an app can keep an expensive commit off
|
||||
// the per-motion `on_change` path.
|
||||
if let Some( slider_idx ) = released_slider
|
||||
{
|
||||
self.scroll_drag_started = false;
|
||||
self.start = None;
|
||||
if let Some( w ) = find_widget( widget_rects, slider_idx )
|
||||
{
|
||||
let value = w.handlers.slider_value_from_pos( w.rect, pos );
|
||||
if let Some( msg ) = w.handlers.slider_release_msg( value )
|
||||
{
|
||||
events.push( ReleaseEvent::PushMsg( msg ) );
|
||||
}
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ enum Msg
|
||||
{
|
||||
Pressed,
|
||||
LongPressed,
|
||||
SliderReleased,
|
||||
}
|
||||
|
||||
fn rect( x: f32, y: f32, w: f32, h: f32 ) -> Rect
|
||||
@@ -48,6 +49,33 @@ fn button_full(
|
||||
}
|
||||
}
|
||||
|
||||
fn slider_widget( idx: usize, r: Rect, on_release: Option<Msg> ) -> LaidOutWidget<Msg>
|
||||
{
|
||||
LaidOutWidget
|
||||
{
|
||||
rect: r,
|
||||
flat_idx: idx,
|
||||
id: None,
|
||||
paint_rect: r,
|
||||
handlers: WidgetHandlers::Slider
|
||||
{
|
||||
on_change: None,
|
||||
on_release: on_release.map( |m| -> std::sync::Arc<dyn Fn( f32 ) -> Msg>
|
||||
{
|
||||
std::sync::Arc::new( move |_| m.clone() )
|
||||
} ),
|
||||
axis: crate::widget::slider::SliderAxis::Horizontal,
|
||||
value: 0.0,
|
||||
thumb_px: 0.0,
|
||||
},
|
||||
keyboard_focusable: true,
|
||||
cursor: crate::types::CursorShape::Default,
|
||||
tooltip: None,
|
||||
accessible_label: None,
|
||||
is_live_region: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn cfg_full( w: u32, h: u32 ) -> SwipeConfig
|
||||
{
|
||||
SwipeConfig
|
||||
@@ -546,6 +574,18 @@ fn release_after_slider_drag_emits_no_events()
|
||||
assert!( g.dragging_slider.is_none() );
|
||||
}
|
||||
|
||||
#[ test ]
|
||||
fn release_after_slider_drag_emits_release_msg()
|
||||
{
|
||||
let widgets = vec![ slider_widget( 1, rect( 0.0, 0.0, 100.0, 20.0 ), Some( Msg::SliderReleased ) ) ];
|
||||
let mut g = GestureState::<Msg>::new();
|
||||
g.start = Some( pt( 50.0, 10.0 ) );
|
||||
g.dragging_slider = Some( 1 );
|
||||
let events = g.on_release( pt( 50.0, 10.0 ), &widgets, &cfg_full( 800, 1200 ), false );
|
||||
assert!( matches!( events.as_slice(), [ ReleaseEvent::PushMsg( Msg::SliderReleased ) ] ) );
|
||||
assert!( g.dragging_slider.is_none() );
|
||||
}
|
||||
|
||||
#[ test ]
|
||||
fn release_after_consumed_scroll_emits_no_events()
|
||||
{
|
||||
|
||||
@@ -327,6 +327,7 @@ impl<Msg: Clone> Element<Msg>
|
||||
WidgetHandlers::Slider
|
||||
{
|
||||
on_change: s.on_change.clone(),
|
||||
on_release: s.on_release.clone(),
|
||||
axis: slider::SliderAxis::Horizontal,
|
||||
value: s.value,
|
||||
// Design-px fallback; the layout pass overwrites this with
|
||||
@@ -339,6 +340,7 @@ impl<Msg: Clone> Element<Msg>
|
||||
WidgetHandlers::Slider
|
||||
{
|
||||
on_change: s.on_change.clone(),
|
||||
on_release: s.on_release.clone(),
|
||||
axis: slider::SliderAxis::Vertical,
|
||||
value: s.value,
|
||||
// The vertical axis maps against the full rect height and
|
||||
|
||||
@@ -76,6 +76,10 @@ pub enum WidgetHandlers<Msg: Clone>
|
||||
Slider
|
||||
{
|
||||
on_change: Option<Arc<dyn Fn( f32 ) -> Msg>>,
|
||||
/// Fired once when the drag ends, with the final value. Lets an app
|
||||
/// keep an expensive commit (a subprocess, a D-Bus round trip, a
|
||||
/// compositor reconfigure) off the per-motion path.
|
||||
on_release: Option<Arc<dyn Fn( f32 ) -> Msg>>,
|
||||
axis: slider::SliderAxis,
|
||||
value: f32,
|
||||
/// Thumb size resolved through the widget-scaling mode at layout
|
||||
@@ -142,9 +146,16 @@ impl<Msg: Clone> Clone for WidgetHandlers<Msg>
|
||||
password_toggle_msg: password_toggle_msg.clone(),
|
||||
}
|
||||
}
|
||||
WidgetHandlers::Slider { on_change, axis, value, thumb_px } =>
|
||||
WidgetHandlers::Slider { on_change, on_release, axis, value, thumb_px } =>
|
||||
{
|
||||
WidgetHandlers::Slider { on_change: on_change.clone(), axis: *axis, value: *value, thumb_px: *thumb_px }
|
||||
WidgetHandlers::Slider
|
||||
{
|
||||
on_change: on_change.clone(),
|
||||
on_release: on_release.clone(),
|
||||
axis: *axis,
|
||||
value: *value,
|
||||
thumb_px: *thumb_px,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -247,6 +258,16 @@ impl<Msg: Clone> WidgetHandlers<Msg>
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the `on_release` message for a Slider given its final value.
|
||||
pub fn slider_release_msg( &self, value: f32 ) -> Option<Msg>
|
||||
{
|
||||
match self
|
||||
{
|
||||
WidgetHandlers::Slider { on_release: Some( f ), .. } => Some( f( value ) ),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute the `[0.0, 1.0]` value for the slider this handler belongs to,
|
||||
/// given a pointer position inside its layout rect. Dispatches on the
|
||||
/// stored [`slider::SliderAxis`] so the same call site in `input.rs` drives
|
||||
|
||||
@@ -280,36 +280,56 @@ impl<Msg: Clone> ListItem<Msg>
|
||||
rect.x + pad_h
|
||||
};
|
||||
|
||||
canvas.draw_text( &self.label, text_x, label_y, label_size, label_color );
|
||||
|
||||
if let Some( ref sub ) = self.subtitle
|
||||
{
|
||||
let sub_size = canvas.font_px( theme::SUBTITLE_SIZE );
|
||||
let sub_y = rect.y + rect.height * 0.62 + sub_size * 0.3;
|
||||
canvas.draw_text( sub, text_x, sub_y, sub_size, subtitle_color );
|
||||
}
|
||||
|
||||
// The trailing slots are laid out before the label is painted: the
|
||||
// label has to be elided against the space they leave, or a long
|
||||
// title runs under the disclosure arrow.
|
||||
let mut trail_right = rect.x + rect.width - pad_h;
|
||||
let icon_gap = canvas.geom_px( theme::ICON_GAP );
|
||||
|
||||
if let Some( ( rgba, w, h ) ) = &self.trailing_icon
|
||||
let mut trail_icon_rect = None;
|
||||
if self.trailing_icon.is_some()
|
||||
{
|
||||
let icon_size = canvas.geom_px( theme::TRAILING_ICON_SIZE );
|
||||
let icon_rect = Rect
|
||||
let r = Rect
|
||||
{
|
||||
x: trail_right - icon_size,
|
||||
y: rect.y + ( rect.height - icon_size ) / 2.0,
|
||||
width: icon_size,
|
||||
height: icon_size,
|
||||
};
|
||||
canvas.draw_image_data( rgba, *w, *h, icon_rect, 1.0 );
|
||||
trail_right = icon_rect.x - canvas.geom_px( theme::ICON_GAP );
|
||||
trail_right = r.x - icon_gap;
|
||||
trail_icon_rect = Some( r );
|
||||
}
|
||||
|
||||
let mut trail_text = None;
|
||||
if let Some( ref trail ) = self.trailing
|
||||
{
|
||||
let trail_size = canvas.font_px( theme::TRAILING_SIZE );
|
||||
let tw = canvas.measure_text( trail, trail_size );
|
||||
let tx = trail_right - tw;
|
||||
let tx = trail_right - canvas.measure_text( trail, trail_size );
|
||||
trail_right = tx - icon_gap;
|
||||
trail_text = Some( ( tx, trail_size ) );
|
||||
}
|
||||
|
||||
let text_max = ( trail_right - text_x ).max( 0.0 );
|
||||
|
||||
let label_text = elide( canvas, &self.label, label_size, text_max );
|
||||
canvas.draw_text( &label_text, text_x, label_y, label_size, label_color );
|
||||
|
||||
if let Some( ref sub ) = self.subtitle
|
||||
{
|
||||
let sub_size = canvas.font_px( theme::SUBTITLE_SIZE );
|
||||
let sub_y = rect.y + rect.height * 0.62 + sub_size * 0.3;
|
||||
let sub_text = elide( canvas, sub, sub_size, text_max );
|
||||
canvas.draw_text( &sub_text, text_x, sub_y, sub_size, subtitle_color );
|
||||
}
|
||||
|
||||
if let ( Some( r ), Some( ( rgba, w, h ) ) ) = ( trail_icon_rect, &self.trailing_icon )
|
||||
{
|
||||
canvas.draw_image_data( rgba, *w, *h, r, 1.0 );
|
||||
}
|
||||
|
||||
if let ( Some( ( tx, trail_size ) ), Some( trail ) ) = ( trail_text, &self.trailing )
|
||||
{
|
||||
let ty = rect.y + ( rect.height + trail_size ) / 2.0 - 2.0;
|
||||
canvas.draw_text( trail, tx, ty, trail_size, trailing_color );
|
||||
}
|
||||
@@ -337,6 +357,35 @@ impl<Msg: Clone> ListItem<Msg>
|
||||
}
|
||||
}
|
||||
|
||||
/// Shorten `text` with a trailing ellipsis so it fits `max_w`, mirroring the
|
||||
/// single-line truncation [`super::text::Text`] applies.
|
||||
fn elide( canvas: &Canvas, text: &str, size: f32, max_w: f32 ) -> String
|
||||
{
|
||||
if max_w <= 0.0
|
||||
{
|
||||
return String::new();
|
||||
}
|
||||
if canvas.measure_text( text, size ) <= max_w
|
||||
{
|
||||
return text.to_string();
|
||||
}
|
||||
|
||||
let ellipsis = "...";
|
||||
let budget = max_w - canvas.measure_text( ellipsis, size );
|
||||
if budget <= 0.0
|
||||
{
|
||||
return ellipsis.to_string();
|
||||
}
|
||||
|
||||
let mut accum = 0.0_f32;
|
||||
let kept: String = text.chars().take_while( |ch|
|
||||
{
|
||||
accum += canvas.measure_text( &ch.to_string(), size );
|
||||
accum <= budget
|
||||
} ).collect();
|
||||
format!( "{kept}{ellipsis}" )
|
||||
}
|
||||
|
||||
/// Create a [`ListItem`] with the given primary label.
|
||||
///
|
||||
/// Add detail and behaviour through the chained builders. For a
|
||||
|
||||
@@ -134,6 +134,9 @@ pub struct Slider<Msg: Clone>
|
||||
/// `Arc` (not `Box`) so the layout pass can clone it into the per-leaf
|
||||
/// handler snapshot for O(1) dispatch on input events.
|
||||
pub( crate ) on_change: Option<Arc<dyn Fn(f32) -> Msg>>,
|
||||
/// Callback invoked once with the final value when the drag ends, so an
|
||||
/// app can keep an expensive commit off the per-motion path.
|
||||
pub( crate ) on_release: Option<Arc<dyn Fn(f32) -> Msg>>,
|
||||
/// Theme slot id for the track background pill. Defaults to the
|
||||
/// generic `surface-slider-track`; override per-instance to opt
|
||||
/// into the `-flat` (no per-surface backdrop) variant when the
|
||||
@@ -165,6 +168,7 @@ impl<Msg: Clone> Slider<Msg>
|
||||
{
|
||||
value: value.clamp( 0.0, 1.0 ),
|
||||
on_change: None,
|
||||
on_release: None,
|
||||
track_surface: theme::SURFACE_TRACK,
|
||||
fill_surface: theme::SURFACE_FILL,
|
||||
accent_thumb: false,
|
||||
@@ -190,6 +194,15 @@ impl<Msg: Clone> Slider<Msg>
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the callback invoked once when the drag ends, with the final
|
||||
/// value. Pair it with [`Self::on_change`] to move the slider without
|
||||
/// paying for the commit on every motion event.
|
||||
pub fn on_release( mut self, f: impl Fn(f32) -> Msg + 'static ) -> Self
|
||||
{
|
||||
self.on_release = Some( Arc::new( f ) );
|
||||
self
|
||||
}
|
||||
|
||||
/// Override the theme slot id used to paint the track background
|
||||
/// — pass `surface-slider-track-flat` (or any other surface) to
|
||||
/// drop the per-instance backdrop blur when the slider already
|
||||
@@ -407,10 +420,16 @@ impl<Msg: Clone> Slider<Msg>
|
||||
let mapper = Arc::clone( f );
|
||||
Arc::new( move |v| ( *mapper )( ( *old )( v ) ) )
|
||||
} );
|
||||
let on_release = self.on_release.map( |old| -> Arc<dyn Fn( f32 ) -> U>
|
||||
{
|
||||
let mapper = Arc::clone( f );
|
||||
Arc::new( move |v| ( *mapper )( ( *old )( v ) ) )
|
||||
} );
|
||||
Slider
|
||||
{
|
||||
value: self.value,
|
||||
on_change,
|
||||
on_release,
|
||||
track_surface: self.track_surface,
|
||||
fill_surface: self.fill_surface,
|
||||
accent_thumb: self.accent_thumb,
|
||||
|
||||
@@ -73,6 +73,9 @@ pub struct VSlider<Msg: Clone>
|
||||
/// dragged. `Arc` (not `Box`) so the layout pass can clone it into the
|
||||
/// per-leaf handler snapshot for O(1) dispatch on input events.
|
||||
pub( crate ) on_change: Option<Arc<dyn Fn(f32) -> Msg>>,
|
||||
/// Callback invoked once with the final value when the drag ends, so an
|
||||
/// app can keep an expensive commit off the per-motion path.
|
||||
pub( crate ) on_release: Option<Arc<dyn Fn(f32) -> Msg>>,
|
||||
/// Theme slot id for the unfilled track. Defaults to
|
||||
/// `surface-slider-track`. Override with [`VSlider::track_surface`]
|
||||
/// when the slider lives inside a panel that already provides its
|
||||
@@ -96,6 +99,7 @@ impl<Msg: Clone> VSlider<Msg>
|
||||
width: Length::widget( theme::WIDTH ),
|
||||
height: Length::widget( theme::HEIGHT ),
|
||||
on_change: None,
|
||||
on_release: None,
|
||||
track_surface: theme::SURFACE_TRACK,
|
||||
fill_surface: theme::SURFACE_FILL,
|
||||
}
|
||||
@@ -135,6 +139,14 @@ impl<Msg: Clone> VSlider<Msg>
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the callback invoked once when the drag ends, with the final
|
||||
/// value. Counterpart of [`Self::on_change`] for expensive commits.
|
||||
pub fn on_release( mut self, f: impl Fn(f32) -> Msg + 'static ) -> Self
|
||||
{
|
||||
self.on_release = Some( Arc::new( f ) );
|
||||
self
|
||||
}
|
||||
|
||||
/// Return the preferred `(width, height)`. `max_width` is ignored — see
|
||||
/// the type-level docs on intrinsic sizing.
|
||||
pub fn preferred_size( &self, _max_width: f32, canvas: &Canvas ) -> (f32, f32)
|
||||
@@ -295,12 +307,18 @@ impl<Msg: Clone> VSlider<Msg>
|
||||
let mapper = Arc::clone( f );
|
||||
Arc::new( move |v| ( *mapper )( ( *old )( v ) ) )
|
||||
} );
|
||||
let on_release = self.on_release.map( |old| -> Arc<dyn Fn( f32 ) -> U>
|
||||
{
|
||||
let mapper = Arc::clone( f );
|
||||
Arc::new( move |v| ( *mapper )( ( *old )( v ) ) )
|
||||
} );
|
||||
VSlider
|
||||
{
|
||||
value: self.value,
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
on_change,
|
||||
on_release,
|
||||
track_surface: self.track_surface,
|
||||
fill_surface: self.fill_surface,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user