Skip to main content

agg_gui/widgets/slider/
mod.rs

1//! `Slider` — a range slider with a draggable thumb.
2//!
3//! Supports linear and logarithmic value mapping (including ranges that span
4//! zero and infinity), three clamping modes, integer/step snapping, "smart aim"
5//! round-value snapping, an optional value suffix, horizontal or vertical
6//! orientation, a toggleable trailing fill, and circle/rect handle shapes.
7//!
8//! The numeric core (value ↔ normalized-position mapping and smart aim) lives
9//! in the sibling [`crate::widgets::slider_math`] module so it can be unit
10//! tested in isolation and to keep this file within the project's size limit.
11
12use std::cell::Cell;
13use std::rc::Rc;
14use std::sync::Arc;
15
16use crate::draw_ctx::DrawCtx;
17use crate::event::{Event, EventResult, Key, MouseButton};
18use crate::geometry::{Rect, Size};
19use crate::layout_props::{HAnchor, Insets, VAnchor, WidgetBase};
20use crate::text::Font;
21use crate::widget::{paint_subtree, Widget};
22use crate::widgets::label::{Label, LabelAlign};
23use crate::widgets::slider_math::{
24    best_in_range_f64, clamp_value_to_range, normalized_from_value, value_from_normalized,
25    SliderSpec,
26};
27
28mod paint;
29
30/// Orientation of a [`Slider`]. Horizontal is the default.
31#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
32pub enum SliderOrientation {
33    Horizontal,
34    Vertical,
35}
36
37/// When a [`Slider`] clamps values to its range. Mirrors egui's `SliderClamping`.
38#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
39pub enum SliderClamping {
40    /// Never clamp — the value may sit outside the range (thumb pins to the edge).
41    Never,
42    /// Only clamp values produced by interacting with the slider; pre-existing
43    /// out-of-range values are left intact.
44    Edits,
45    /// Always clamp, including a pre-existing out-of-range value.
46    #[default]
47    Always,
48}
49
50/// Shape of a [`Slider`]'s draggable handle.
51#[derive(Clone, Copy, Debug, PartialEq)]
52pub enum HandleShape {
53    /// A circle (the default).
54    Circle,
55    /// A rectangle whose extent along the slider axis is `aspect_ratio` times
56    /// its cross-axis extent.
57    Rect { aspect_ratio: f64 },
58}
59
60impl Default for HandleShape {
61    fn default() -> Self {
62        Self::Circle
63    }
64}
65
66/// Pixels of "aim radius" used when smart aim is on: the drag samples the value
67/// a little to each side of the pointer and snaps to the roundest value between.
68/// Deviation from egui: there this comes from `InputState::aim_radius()` (varies
69/// per input device); we use a fixed mouse-like radius since agg-gui's event
70/// layer doesn't carry per-device precision yet.
71const AIM_RADIUS: f64 = 1.5;
72/// Track length (px) of a vertical slider.
73const VERT_LEN: f64 = 140.0;
74
75const TRACK_H: f64 = 4.0;
76const THUMB_R: f64 = 7.0;
77/// Total widget height.  Needs to fit the thumb (diameter `2 * THUMB_R`)
78/// plus a little breathing room for the focus ring — `22 px` keeps rows
79/// compact in settings-style panels while still being easy to grab.
80const WIDGET_H: f64 = 22.0;
81/// Default pixel budget reserved on the right for the numeric value
82/// label.  Wide enough for 4-5 glyphs at the slider's default font
83/// size.  Set to `0.0` via [`Slider::with_show_value(false)`] to hide.
84const VALUE_W: f64 = 44.0;
85/// Gap between the track's right edge and the value label's left edge.
86const VALUE_GAP: f64 = 6.0;
87
88/// Inspector-visible properties of a [`Slider`].
89///
90/// **The "companion props" pattern:** widgets that opt into the reflection-
91/// driven inspector hold a small `*Props` struct exposing only their
92/// directly-editable values.  This sidesteps two structural problems with
93/// deriving `Reflect` on the whole widget:
94///   1. `bevy_reflect::Reflect` requires `Send + Sync`, which widgets violate
95///      because they carry `Rc<Cell<…>>` and non-`Sync` callbacks.
96///   2. Sub-widgets (`Label` here) and `Arc<Font>` would force a cascading
97///      `Reflect` derive across types that don't have it.
98///
99/// The companion struct contains plain values (`f64`, `bool`, `Option<usize>`)
100/// — `Send + Sync + Reflect`-friendly — and the widget routes all reads/writes
101/// through it.  The inspector edits the companion live and the widget reacts.
102#[cfg_attr(feature = "reflect", derive(bevy_reflect::Reflect))]
103#[derive(Clone, Debug)]
104pub struct SliderProps {
105    pub value: f64,
106    pub min: f64,
107    pub max: f64,
108    pub step: f64,
109    pub show_value: bool,
110    /// Fixed decimals for the value label — overrides the step-based
111    /// auto-format when `Some`.
112    pub decimals: Option<usize>,
113    pub font_size: f64,
114}
115
116impl Default for SliderProps {
117    fn default() -> Self {
118        Self {
119            value: 0.0,
120            min: 0.0,
121            max: 1.0,
122            step: 0.01,
123            show_value: true,
124            decimals: None,
125            font_size: 12.0,
126        }
127    }
128}
129
130/// A horizontal slider for a `f64` value within `[min, max]`.
131pub struct Slider {
132    bounds: Rect,
133    children: Vec<Box<dyn Widget>>, // always empty
134    base: WidgetBase,
135    /// Reflectable, inspector-editable values — see [`SliderProps`].
136    pub props: SliderProps,
137    dragging: bool,
138    focused: bool,
139    hovered: bool,
140    on_change: Option<Box<dyn FnMut(f64)>>,
141    /// Optional external mirror of `value`.  When `Some`, `layout()` re-reads
142    /// the cell every frame so a second widget that writes the same cell
143    /// drives this slider live; `set_value` writes back.  Mirrors the
144    /// `ToggleSwitch::with_state_cell` pattern — the cell is the source-of-
145    /// truth so multiple widgets can reflect the same value bidirectionally.
146    value_cell: Option<Rc<Cell<f64>>>,
147    /// Backbuffered Label that renders the numeric value.  Updated in
148    /// `layout()` with the current formatted value so the text follows
149    /// drags live.
150    value_label: Label,
151    /// Tracks the string last pushed into `value_label` so we only
152    /// invalidate its cache when the displayed value actually changes.
153    last_value_text: String,
154
155    // ── Extended configuration (egui-parity options) ──────────────────────
156    /// Log/linear mapping spec. `logarithmic` off by default.
157    spec: SliderSpec,
158    clamping: SliderClamping,
159    smart_aim: bool,
160    orientation: SliderOrientation,
161    handle_shape: HandleShape,
162    /// When true, values are rounded to integers and formatted with no decimals.
163    integer: bool,
164    /// Text appended after the numeric value (e.g. "°" or " m").
165    suffix: String,
166    /// Whether to paint the accent-colored fill up to the handle. Defaults to
167    /// `true` to preserve the widget's historical appearance across the app;
168    /// egui's per-slider default is off, so demos set this explicitly.
169    trailing_fill: bool,
170}
171
172impl Slider {
173    pub fn new(value: f64, min: f64, max: f64, font: Arc<Font>) -> Self {
174        // Not `f64::clamp` — that panics when min > max, and reversed
175        // (high-to-low) ranges are supported by the mapping in `slider_math`.
176        let v = clamp_value_to_range(value, min, max);
177        // Default step is 1/100th of the span, but an infinite/degenerate range
178        // (e.g. the logarithmic `-∞..=∞` sliders in the Sliders demo) makes this
179        // `∞` or `NaN`; fall back to "no step" so `commit` never step-snaps into
180        // a NaN. Callers set an explicit finite step via `with_step`.
181        let default_step = {
182            let s = (max - min) / 100.0;
183            if s.is_finite() {
184                s
185            } else {
186                0.0
187            }
188        };
189        let font_size = 12.0;
190        let value_label = Label::new("", Arc::clone(&font))
191            .with_font_size(font_size)
192            .with_align(LabelAlign::Right);
193        Self {
194            bounds: Rect::default(),
195            children: Vec::new(),
196            base: WidgetBase::new(),
197            props: SliderProps {
198                value: v,
199                min,
200                max,
201                step: default_step,
202                show_value: true,
203                decimals: None,
204                font_size,
205            },
206            dragging: false,
207            focused: false,
208            hovered: false,
209            on_change: None,
210            value_cell: None,
211            value_label,
212            last_value_text: String::new(),
213            spec: SliderSpec::default(),
214            clamping: SliderClamping::default(),
215            smart_aim: true,
216            orientation: SliderOrientation::Horizontal,
217            handle_shape: HandleShape::Circle,
218            integer: false,
219            suffix: String::new(),
220            trailing_fill: true,
221        }
222    }
223
224    pub fn with_step(mut self, step: f64) -> Self {
225        self.props.step = step;
226        self
227    }
228
229    // ── Extended-configuration builders ────────────────────────────────────
230
231    /// Enable/disable logarithmic value mapping. Great for spanning huge ranges
232    /// (and, unlike naive log, tolerates ranges that include zero and infinity).
233    pub fn with_logarithmic(mut self, logarithmic: bool) -> Self {
234        self.spec.logarithmic = logarithmic;
235        self
236    }
237
238    /// For logarithmic sliders including zero: the smallest positive value the
239    /// user can select. Default `1e-6` (or `1` for integer sliders).
240    pub fn with_smallest_positive(mut self, smallest_positive: f64) -> Self {
241        self.spec.smallest_positive = smallest_positive;
242        self
243    }
244
245    /// For logarithmic sliders whose high end is infinity: the largest finite
246    /// value before the slider switches to `∞`. Default `∞`.
247    pub fn with_largest_finite(mut self, largest_finite: f64) -> Self {
248        self.spec.largest_finite = largest_finite;
249        self
250    }
251
252    /// Choose when values are clamped to the range. Default [`SliderClamping::Always`].
253    pub fn with_clamping(mut self, clamping: SliderClamping) -> Self {
254        self.clamping = clamping;
255        self
256    }
257
258    /// Turn smart aim (snap toward round values while dragging) on/off. Default on.
259    pub fn with_smart_aim(mut self, smart_aim: bool) -> Self {
260        self.smart_aim = smart_aim;
261        self
262    }
263
264    /// Horizontal or vertical. Default horizontal.
265    pub fn with_orientation(mut self, orientation: SliderOrientation) -> Self {
266        self.orientation = orientation;
267        self
268    }
269
270    /// Change the handle shape (circle or aspect-ratio'd rectangle).
271    pub fn with_handle_shape(mut self, shape: HandleShape) -> Self {
272        self.handle_shape = shape;
273        self
274    }
275
276    /// Make this an integer slider: values round to whole numbers, formatting
277    /// uses no decimals, and logarithmic mapping treats `1` as the smallest
278    /// positive value.
279    pub fn with_integer(mut self, integer: bool) -> Self {
280        self.integer = integer;
281        if integer {
282            self.spec.smallest_positive = 1.0;
283        }
284        self
285    }
286
287    /// Append a suffix to the value label (e.g. "°" or " m").
288    pub fn with_suffix(mut self, suffix: impl Into<String>) -> Self {
289        self.suffix = suffix.into();
290        self
291    }
292
293    /// Toggle the accent-colored trailing fill painted up to the handle.
294    pub fn with_trailing_fill(mut self, trailing_fill: bool) -> Self {
295        self.trailing_fill = trailing_fill;
296        self
297    }
298
299    /// Bind this slider's value to an external `Rc<Cell<f64>>`.
300    ///
301    /// The cell becomes the source-of-truth: `layout()` reads it every
302    /// frame so any other widget (or code path) that writes the cell
303    /// will drive this slider live; drag interactions here write back
304    /// to the cell too.  Pattern mirrors `ToggleSwitch::with_state_cell`.
305    pub fn with_value_cell(mut self, cell: Rc<Cell<f64>>) -> Self {
306        // Reversed-range/NaN tolerant — see the note in `new`.
307        self.props.value = clamp_value_to_range(cell.get(), self.props.min, self.props.max);
308        self.value_cell = Some(cell);
309        self
310    }
311    pub fn with_show_value(mut self, show: bool) -> Self {
312        self.props.show_value = show;
313        self
314    }
315
316    /// Force a specific decimal count for the numeric value label.  When
317    /// unset, the format falls back to a heuristic based on `step`.
318    pub fn with_decimals(mut self, decimals: usize) -> Self {
319        self.props.decimals = Some(decimals);
320        self
321    }
322
323    pub fn with_margin(mut self, m: Insets) -> Self {
324        self.base.margin = m;
325        self
326    }
327    pub fn with_h_anchor(mut self, h: HAnchor) -> Self {
328        self.base.h_anchor = h;
329        self
330    }
331    pub fn with_v_anchor(mut self, v: VAnchor) -> Self {
332        self.base.v_anchor = v;
333        self
334    }
335    pub fn with_min_size(mut self, s: Size) -> Self {
336        self.base.min_size = s;
337        self
338    }
339    pub fn with_max_size(mut self, s: Size) -> Self {
340        self.base.max_size = s;
341        self
342    }
343
344    pub fn on_change(mut self, cb: impl FnMut(f64) + 'static) -> Self {
345        self.on_change = Some(Box::new(cb));
346        self
347    }
348
349    pub fn value(&self) -> f64 {
350        self.props.value
351    }
352
353    pub fn set_value(&mut self, v: f64) {
354        // Reject NaN so one bad write can't permanently poison the value (and,
355        // through a shared value cell, other widgets bound to it).
356        if v.is_nan() {
357            return;
358        }
359        self.props.value = self.commit(v);
360        if let Some(cell) = &self.value_cell {
361            cell.set(self.props.value);
362        }
363    }
364
365    fn fire(&mut self) {
366        let v = self.props.value;
367        if let Some(cell) = &self.value_cell {
368            cell.set(v);
369        }
370        if let Some(cb) = self.on_change.as_mut() {
371            cb(v);
372        }
373    }
374
375    fn is_vertical(&self) -> bool {
376        matches!(self.orientation, SliderOrientation::Vertical)
377    }
378
379    /// Handle radius along the slider axis — the range is shrunk by this so the
380    /// handle never overhangs the ends.
381    fn handle_extent(&self) -> f64 {
382        match self.handle_shape {
383            HandleShape::Circle => THUMB_R,
384            HandleShape::Rect { aspect_ratio } => THUMB_R * aspect_ratio,
385        }
386    }
387
388    /// Pixel X of the track's right edge (horizontal).  The value label lives in
389    /// a reserved strip to the right of this so a thumb at max doesn't overdraw
390    /// the digits.
391    fn track_right(&self) -> f64 {
392        let reserved = if self.props.show_value {
393            VALUE_W + VALUE_GAP
394        } else {
395            0.0
396        };
397        (self.bounds.width - reserved - THUMB_R).max(THUMB_R + 1.0)
398    }
399
400    /// The pixel positions (along the main axis) of normalized `0.0` and `1.0`.
401    /// For vertical sliders the larger value maps to the top (smaller y).
402    fn position_range(&self) -> (f64, f64) {
403        let hr = self.handle_extent();
404        if self.is_vertical() {
405            let bottom = self.bounds.height - hr; // normalized 0
406            let top = hr; // normalized 1
407            (bottom, top)
408        } else {
409            (THUMB_R, self.track_right()) // normalized 0..1
410        }
411    }
412
413    fn normalized(&self) -> f64 {
414        normalized_from_value(self.props.value, self.props.min, self.props.max, &self.spec)
415    }
416
417    /// Center of the thumb along the main axis (px).
418    fn thumb_pos(&self) -> f64 {
419        let (p0, p1) = self.position_range();
420        p0 + self.normalized().clamp(0.0, 1.0) * (p1 - p0)
421    }
422
423    fn value_from_pixel(&self, px: f64) -> f64 {
424        let (p0, p1) = self.position_range();
425        let n = if (p1 - p0).abs() < f64::EPSILON {
426            0.0
427        } else {
428            ((px - p0) / (p1 - p0)).clamp(0.0, 1.0)
429        };
430        value_from_normalized(n, self.props.min, self.props.max, &self.spec)
431    }
432
433    /// Apply clamping, step snapping and integer rounding to a raw value.
434    ///
435    /// Rejects `NaN` outright (returning the current value) so a single bad
436    /// input can never poison the stored state — see also `set_value` and the
437    /// value-cell read in `layout`.
438    fn commit(&self, mut value: f64) -> f64 {
439        if value.is_nan() {
440            return self.props.value;
441        }
442        if self.clamping != SliderClamping::Never {
443            value = clamp_value_to_range(value, self.props.min, self.props.max);
444        }
445        // Step-snapping only makes sense with a finite step *and* a finite
446        // origin: an infinite bound (`min = -∞`, as on the demo's log range
447        // sliders) turns the arithmetic below into `NaN`.
448        if self.props.step > 0.0 && self.props.step.is_finite() && self.props.min.is_finite() {
449            let start = self.props.min;
450            value = start + ((value - start) / self.props.step).round() * self.props.step;
451        }
452        if self.integer {
453            value = value.round();
454        }
455        value
456    }
457
458    /// Value the pointer maps to, with smart aim applied when enabled.
459    fn pointer_value(&self, px: f64) -> f64 {
460        let raw = if self.smart_aim {
461            best_in_range_f64(
462                self.value_from_pixel(px - AIM_RADIUS),
463                self.value_from_pixel(px + AIM_RADIUS),
464            )
465        } else {
466            self.value_from_pixel(px)
467        };
468        self.commit(raw)
469    }
470
471    /// Move the value one keyboard step in `dir` (+1 increases the value).
472    fn nudge(&self, dir: f64) -> f64 {
473        if self.props.step > 0.0 {
474            return self.commit(self.props.value + dir * self.props.step);
475        }
476        let (p0, p1) = self.position_range();
477        let cur = self.thumb_pos();
478        let px = cur + dir * (p1 - p0).signum();
479        let raw = if self.smart_aim {
480            best_in_range_f64(
481                self.value_from_pixel(px - 0.49),
482                self.value_from_pixel(px + 0.49),
483            )
484        } else {
485            self.value_from_pixel(px)
486        };
487        self.commit(raw)
488    }
489
490    /// Number of decimals to show when no explicit `decimals` override is set.
491    fn auto_decimals(&self) -> usize {
492        if self.integer {
493            return 0;
494        }
495        if self.props.step >= 1.0 {
496            0
497        } else if self.props.step >= 0.1 {
498            1
499        } else if self.props.step >= 0.01 {
500            2
501        } else if self.props.step > 0.0 {
502            3
503        } else {
504            // No step (e.g. logarithmic): pick precision from magnitude so a
505            // value of 10000 shows no decimals and 0.001 shows several.
506            let v = self.props.value.abs();
507            if v == 0.0 || !v.is_finite() {
508                2
509            } else {
510                (3 - v.log10().floor() as i32).clamp(0, 6) as usize
511            }
512        }
513    }
514
515    /// Format the slider's value (decimals + optional suffix).
516    fn format_value(&self) -> String {
517        let decimals = self.props.decimals.unwrap_or_else(|| self.auto_decimals());
518        format!("{:.*}{}", decimals, self.props.value, self.suffix)
519    }
520
521}
522
523impl Widget for Slider {
524    fn type_name(&self) -> &'static str {
525        "Slider"
526    }
527    fn bounds(&self) -> Rect {
528        self.bounds
529    }
530    fn set_bounds(&mut self, b: Rect) {
531        self.bounds = b;
532    }
533    fn children(&self) -> &[Box<dyn Widget>] {
534        &self.children
535    }
536    fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
537        &mut self.children
538    }
539
540    #[cfg(feature = "reflect")]
541    fn as_reflect(&self) -> Option<&dyn bevy_reflect::Reflect> {
542        Some(&self.props)
543    }
544    #[cfg(feature = "reflect")]
545    fn as_reflect_mut(&mut self) -> Option<&mut dyn bevy_reflect::Reflect> {
546        Some(&mut self.props)
547    }
548
549    fn is_focusable(&self) -> bool {
550        true
551    }
552
553    fn margin(&self) -> Insets {
554        self.base.margin
555    }
556    fn widget_base(&self) -> Option<&WidgetBase> {
557        Some(&self.base)
558    }
559    fn widget_base_mut(&mut self) -> Option<&mut WidgetBase> {
560        Some(&mut self.base)
561    }
562    fn h_anchor(&self) -> HAnchor {
563        self.base.h_anchor
564    }
565    fn v_anchor(&self) -> VAnchor {
566        self.base.v_anchor
567    }
568    fn min_size(&self) -> Size {
569        self.base.min_size
570    }
571    fn max_size(&self) -> Size {
572        self.base.max_size
573    }
574
575    fn layout(&mut self, available: Size) -> Size {
576        // Re-read external cell every frame — another widget (e.g. the
577        // System window's slider) may have written a new value.  Skip
578        // while dragging so the user's in-flight drag isn't fought
579        // back by rounding inside the source cell.
580        if !self.dragging {
581            if let Some(cell) = &self.value_cell {
582                let raw = cell.get();
583                // Ignore a NaN in the cell — keep the last good value so a single
584                // poisoned frame from another writer can't wedge this slider.
585                if !raw.is_nan() {
586                    self.props.value = if self.clamping == SliderClamping::Always {
587                        clamp_value_to_range(raw, self.props.min, self.props.max)
588                    } else {
589                        raw
590                    };
591                }
592            }
593        }
594
595        // Refresh the value-label text only when the displayed string
596        // actually changed — Label's `set_text` invalidates its cache
597        // so we want to skip this when the value is unchanged (e.g.
598        // idle frames between drags).
599        if self.props.show_value {
600            let new_text = self.format_value();
601            if new_text != self.last_value_text {
602                self.value_label.set_text(new_text.clone());
603                self.last_value_text = new_text;
604            }
605            // Size the label to exactly the reserved strip; right-align
606            // anchors the digits to the widget's right edge.
607            let lh = self.props.font_size * 1.5;
608            let _ = self.value_label.layout(Size::new(VALUE_W, lh));
609            self.value_label
610                .set_bounds(Rect::new(0.0, 0.0, VALUE_W, lh));
611        }
612
613        if self.is_vertical() {
614            Size::new(available.width, VERT_LEN)
615        } else {
616            Size::new(available.width, WIDGET_H)
617        }
618    }
619
620    fn paint(&mut self, ctx: &mut dyn DrawCtx) {
621        if self.is_vertical() {
622            self.paint_vertical(ctx);
623        } else {
624            self.paint_horizontal(ctx);
625        }
626    }
627
628    fn on_event(&mut self, event: &Event) -> EventResult {
629        match event {
630            Event::MouseMove { pos } => {
631                let was = self.hovered;
632                self.hovered = self.hit_test(*pos);
633                if self.dragging {
634                    let px = if self.is_vertical() { pos.y } else { pos.x };
635                    self.props.value = self.pointer_value(px);
636                    self.fire();
637                    crate::animation::request_draw();
638                    return EventResult::Consumed;
639                }
640                if was != self.hovered {
641                    crate::animation::request_draw();
642                    return EventResult::Consumed;
643                }
644                EventResult::Ignored
645            }
646            Event::MouseDown {
647                button: MouseButton::Left,
648                pos,
649                ..
650            } => {
651                self.dragging = true;
652                let px = if self.is_vertical() { pos.y } else { pos.x };
653                self.props.value = self.pointer_value(px);
654                self.fire();
655                crate::animation::request_draw();
656                EventResult::Consumed
657            }
658            Event::MouseUp {
659                button: MouseButton::Left,
660                ..
661            } => {
662                let was = self.dragging;
663                self.dragging = false;
664                if was {
665                    crate::animation::request_draw();
666                }
667                EventResult::Consumed
668            }
669            Event::KeyDown { key, .. } => {
670                // Arrows move the value along the slider's own axis: left/right
671                // for horizontal, up/down for vertical (up = increase).
672                let dir = match (key, self.is_vertical()) {
673                    (Key::ArrowLeft, false) => -1.0,
674                    (Key::ArrowRight, false) => 1.0,
675                    (Key::ArrowDown, true) => -1.0,
676                    (Key::ArrowUp, true) => 1.0,
677                    _ => 0.0,
678                };
679                if dir != 0.0 {
680                    self.props.value = self.nudge(dir);
681                    self.fire();
682                    crate::animation::request_draw();
683                    EventResult::Consumed
684                } else {
685                    EventResult::Ignored
686                }
687            }
688            Event::FocusGained => {
689                let was = self.focused;
690                self.focused = true;
691                if !was {
692                    crate::animation::request_draw();
693                    EventResult::Consumed
694                } else {
695                    EventResult::Ignored
696                }
697            }
698            Event::FocusLost => {
699                let was_focused = self.focused;
700                let was_dragging = self.dragging;
701                self.focused = false;
702                self.dragging = false;
703                if was_focused || was_dragging {
704                    crate::animation::request_draw();
705                    EventResult::Consumed
706                } else {
707                    EventResult::Ignored
708                }
709            }
710            _ => EventResult::Ignored,
711        }
712    }
713}
714
715#[cfg(test)]
716mod tests;