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:
@@ -326,24 +326,26 @@ impl<Msg: Clone> Element<Msg>
|
||||
{
|
||||
WidgetHandlers::Slider
|
||||
{
|
||||
on_change: s.on_change.clone(),
|
||||
axis: slider::SliderAxis::Horizontal,
|
||||
value: s.value,
|
||||
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
|
||||
// the widget-scaling-resolved size via `set_slider_thumb_px`.
|
||||
thumb_px: slider::thumb_design_px(),
|
||||
thumb_px: slider::thumb_design_px(),
|
||||
}
|
||||
}
|
||||
Element::VSlider( s ) =>
|
||||
{
|
||||
WidgetHandlers::Slider
|
||||
{
|
||||
on_change: s.on_change.clone(),
|
||||
axis: slider::SliderAxis::Vertical,
|
||||
value: s.value,
|
||||
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
|
||||
// takes no thumb inset, so this is unused.
|
||||
thumb_px: 0.0,
|
||||
thumb_px: 0.0,
|
||||
}
|
||||
}
|
||||
_ => WidgetHandlers::None,
|
||||
|
||||
@@ -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