Skip to main content

ravenrs_widgets/imaging/
overlays.rs

1//! What is drawn over a picture: composition guides, the rectangle of a drag, the outline of a
2//! selection, the eyedropper. They share [`ContentSpace`], which says where the content is.
3
4use std::cell::Cell;
5use std::fmt;
6use std::rc::Rc;
7
8use ravenrs_core::{
9    Color, CursorIcon, Key, Modifiers, PathElement, PathFill, Point, Quad, Rect, Role, Scene, Size,
10};
11use ravenrs_layout::{Align, Axis, LayoutStyle};
12use ravenrs_reactive::{Binding, Observer, State};
13use ravenrs_style::{Semantic, Theme};
14
15use super::{ZoomView, stroke_outline};
16use crate::context::{BuildCx, DragPhase, NodeId, Paint, Semantics};
17use crate::{AnyView, ButtonSize, IconButton, IconName, Tooltip, View};
18
19// ---------------------------------------------------------------------------------------------
20// ContentSpace
21// ---------------------------------------------------------------------------------------------
22
23/// How the coordinates of a widget drawn over content relate to the screen, so that a selection
24/// or a crop is told in the units of the content whatever the zoom.
25#[derive(Clone, Debug, Default)]
26#[non_exhaustive]
27#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
28pub enum ContentSpace {
29    /// The widget's own box is the content, one unit per logical pixel from its top left corner.
30    #[default]
31    Frame,
32    /// Content of this size, shown whole and centered in the box, as an image that fits.
33    Fit(Size),
34    /// Content of this size where a [`ZoomPanView`](super::ZoomPanView) with the same
35    /// [`ZoomView`] shows it. Put the widget exactly over that view.
36    View(Binding<ZoomView>, Size),
37}
38
39#[cfg_attr(feature = "live", ravenrs_live::expose)]
40impl ContentSpace {
41    /// Where the whole content lands on screen, for a widget at `frame`.
42    pub fn placed(&self, frame: Rect) -> Rect {
43        match self {
44            Self::Frame => frame,
45            Self::Fit(content) => fitted(frame, *content),
46            Self::View(view, content) => {
47                let mut v = view.get();
48                v.viewport = frame.size;
49                if v.fit {
50                    v.scale = v.fit_scale(*content);
51                    v.center = Point::new(0.5, 0.5);
52                }
53                let placed = v.placed(*content);
54                placed.translate(frame.origin)
55            }
56        }
57    }
58
59    /// The size of the content in its own units.
60    pub fn content_size(&self, frame: Rect) -> Size {
61        match self {
62            Self::Frame => frame.size,
63            Self::Fit(content) | Self::View(_, content) => *content,
64        }
65    }
66
67    /// Logical pixels on screen per unit of content.
68    pub fn scale(&self, frame: Rect) -> f32 {
69        self.map(frame).scale
70    }
71
72    /// A point on screen, in the units of the content.
73    pub fn to_content(&self, frame: Rect, at: Point) -> Point {
74        self.map(frame).content(at)
75    }
76
77    /// A point of the content, on screen.
78    pub fn to_screen(&self, frame: Rect, at: Point) -> Point {
79        self.map(frame).screen(at)
80    }
81
82    /// A rectangle of the content, on screen.
83    pub fn rect_to_screen(&self, frame: Rect, rect: Rect) -> Rect {
84        self.map(frame).screen_rect(rect)
85    }
86
87    pub(super) fn map(&self, frame: Rect) -> ContentMap {
88        let placed = self.placed(frame);
89        let content = self.content_size(frame);
90        ContentMap { placed, scale: placed.width() / content.width.max(f32::EPSILON), content }
91    }
92
93    /// Makes the build that calls it follow the view.
94    pub(super) fn watch(&self) {
95        if let Self::View(view, _) = self {
96            view.get();
97        }
98    }
99}
100
101/// The largest rectangle of the proportions of `content` in `frame`, centered.
102pub(super) fn fitted(frame: Rect, content: Size) -> Rect {
103    let scale = (frame.width() / content.width.max(f32::EPSILON))
104        .min(frame.height() / content.height.max(f32::EPSILON))
105        .max(0.0);
106    let (w, h) = (content.width * scale, content.height * scale);
107    Rect::new(frame.x() + (frame.width() - w) / 2.0, frame.y() + (frame.height() - h) / 2.0, w, h)
108}
109
110/// One frame's worth of [`ContentSpace`].
111#[derive(Clone, Copy, Debug)]
112pub(super) struct ContentMap {
113    pub placed: Rect,
114    pub scale: f32,
115    pub content: Size,
116}
117
118#[cfg_attr(feature = "live", ravenrs_live::expose)]
119impl ContentMap {
120    pub fn screen(&self, at: Point) -> Point {
121        Point::new(self.placed.x() + at.x * self.scale, self.placed.y() + at.y * self.scale)
122    }
123
124    pub fn content(&self, at: Point) -> Point {
125        let scale = self.scale.max(f32::EPSILON);
126        Point::new((at.x - self.placed.x()) / scale, (at.y - self.placed.y()) / scale)
127    }
128
129    pub fn screen_rect(&self, rect: Rect) -> Rect {
130        let at = self.screen(rect.origin);
131        Rect::new(at.x, at.y, rect.width() * self.scale, rect.height() * self.scale)
132    }
133
134    pub fn bounds(&self) -> Rect {
135        Rect::new(0.0, 0.0, self.content.width, self.content.height)
136    }
137}
138
139/// A rectangle from two opposite corners.
140fn spanned(a: Point, b: Point) -> Rect {
141    Rect::new(a.x.min(b.x), a.y.min(b.y), (b.x - a.x).abs(), (b.y - a.y).abs())
142}
143
144/// Two inks that read over any picture together, the light one and the dark one. They come from
145/// the theme but do not swap with it: a picture stays the same in a light theme.
146pub(super) fn picture_inks(theme: &Theme) -> (Color, Color) {
147    let brightness = |c: Color| 0.2126 * c.r + 0.7152 * c.g + 0.0722 * c.b;
148    let (a, b) = (theme.color(Semantic::Canvas), theme.color(Semantic::Ink));
149    if brightness(a) > brightness(b) { (a, b) } else { (b, a) }
150}
151
152// ---------------------------------------------------------------------------------------------
153// The box of an overlay
154// ---------------------------------------------------------------------------------------------
155
156/// The box of a widget drawn over something: the size it was given, or that of the view it is
157/// laid over.
158#[derive(Default)]
159pub(super) struct OverlayBox {
160    width: Option<f32>,
161    height: Option<f32>,
162    grow: Option<f32>,
163    stage: Option<AnyView>,
164    pub space: ContentSpace,
165}
166
167impl fmt::Debug for OverlayBox {
168    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169        f.debug_struct("OverlayBox").field("space", &self.space).finish_non_exhaustive()
170    }
171}
172
173#[cfg_attr(feature = "live", ravenrs_live::expose)]
174impl OverlayBox {
175    pub fn size(&mut self, width: f32, height: f32) {
176        self.width = Some(width);
177        self.height = Some(height);
178        self.grow = Some(0.0);
179    }
180
181    pub fn grow(&mut self, grow: f32) {
182        self.grow = Some(grow);
183    }
184
185    pub fn stage(&mut self, stage: AnyView) {
186        self.stage = Some(stage);
187    }
188
189    /// The size of the box as far as a build can know it, which the first build may not.
190    pub fn known_size(&self, cx: &BuildCx<'_>) -> Option<Size> {
191        match (self.width, self.height) {
192            (Some(width), Some(height)) => Some(Size::new(width, height)),
193            _ => cx.scroll_viewport(),
194        }
195    }
196
197    /// Builds the box, and returns it with the node that paints and takes the pointer.
198    pub fn build(&self, cx: &mut BuildCx<'_>, paint: Paint) -> (NodeId, NodeId) {
199        let sized = self.width.is_some();
200        let Some(stage) = &self.stage else {
201            let grow = self.grow.unwrap_or(1.0);
202            let root = cx.container(
203                LayoutStyle {
204                    width: self.width,
205                    height: self.height,
206                    grow,
207                    align_self: Some(if sized { Align::Start } else { Align::Stretch }),
208                    ..LayoutStyle::leaf()
209                },
210                &[],
211                paint,
212            );
213            cx.report_frame(root);
214            return (root, root);
215        };
216        let stage = cx.child(stage.as_ref());
217        // Laid over: the runtime has no box that takes the size of a sibling, so it is read back
218        // from the last layout. Nothing else moves when the first build has none yet.
219        let measured = cx.scroll_viewport().unwrap_or(Size::ZERO);
220        let surface = cx.container(
221            LayoutStyle {
222                absolute: Some((0.0, 0.0)),
223                width: Some(measured.width),
224                height: Some(measured.height),
225                ..LayoutStyle::leaf()
226            },
227            &[],
228            paint,
229        );
230        let grow = self.grow.unwrap_or(0.0);
231        let root = cx.container(
232            LayoutStyle {
233                width: self.width,
234                height: self.height,
235                grow,
236                align: Align::Stretch,
237                align_self: Some(if grow > 0.0 { Align::Stretch } else { Align::Start }),
238                ..LayoutStyle::stack(Axis::Vertical)
239            },
240            &[stage, surface],
241            Paint::None,
242        );
243        cx.report_frame(root);
244        (root, surface)
245    }
246}
247
248/// The builders every overlay has, over the field that holds its box.
249macro_rules! overlay_builders {
250    ($ty:ident, $($field:ident).+) => {
251        #[cfg_attr(feature = "live", ravenrs_live::expose(part = "builders"))]
252        impl $ty {
253            /// Fixed size in logical pixels, instead of taking the free space.
254            pub fn size(mut self, width: f32, height: f32) -> Self {
255                self.$($field).+.size(width, height);
256                self
257            }
258
259            /// How much of the free space along the parent stack it takes: 1 by default, or 0 over a
260            /// [stage](Self::stage), which then decides the size.
261            pub fn grow(mut self, grow: f32) -> Self {
262                self.$($field).+.grow(grow);
263                self
264            }
265
266            /// The view it goes over, such as an [`Image`](crate::Image) or a
267            /// [`ZoomPanView`](crate::ZoomPanView), which gives it its size. Without one, put it in
268            /// a [`ZStack`](crate::ZStack) with a fixed [size](Self::size).
269            pub fn stage(mut self, stage: impl View + 'static) -> Self {
270                self.$($field).+.stage(Box::new(stage));
271                self
272            }
273
274            /// Where the content is, and so the units of its coordinates: by default, its own box.
275            pub fn space(mut self, space: ContentSpace) -> Self {
276                self.$($field).+.space = space;
277                self
278            }
279        }
280    };
281}
282pub(super) use overlay_builders;
283
284/// A painter that keeps asking for frames during a gesture, which then costs no build, and that
285/// costs nothing afterwards.
286pub(super) fn gesture_paint(live: bool, draw: impl Fn(Rect, &mut Scene) + 'static) -> Paint {
287    if live {
288        Paint::Live(Rc::new(move |frame, _, scene| draw(frame, scene)))
289    } else {
290        Paint::Custom(Rc::new(draw))
291    }
292}
293
294// ---------------------------------------------------------------------------------------------
295// RuleOfThirdsGrid
296// ---------------------------------------------------------------------------------------------
297
298/// The lines a [`RuleOfThirdsGrid`] draws.
299#[derive(Clone, Copy, Debug, Default, PartialEq)]
300#[non_exhaustive]
301#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
302pub enum CompositionGuide {
303    /// Two lines each way, at a third and two thirds.
304    #[default]
305    Thirds,
306    /// This many equal parts each way, such as a fine grid to level a horizon.
307    Divisions(u32),
308    /// Square cells, this many along the shorter side.
309    Squares(u32),
310    /// Two lines each way, at the golden ratio.
311    GoldenRatio,
312}
313
314#[cfg_attr(feature = "live", ravenrs_live::expose)]
315impl CompositionGuide {
316    /// Where the lines cross each axis, as shares of a box of this size.
317    fn lines(self, width: f32, height: f32) -> (Vec<f32>, Vec<f32>) {
318        let equal = |parts: u32| -> Vec<f32> { (1..parts).map(|i| i as f32 / parts as f32).collect() };
319        match self {
320            Self::Thirds => (equal(3), equal(3)),
321            Self::Divisions(parts) => (equal(parts), equal(parts)),
322            Self::GoldenRatio => {
323                let golden = 1.0 / 1.618_034;
324                (vec![1.0 - golden, golden], vec![1.0 - golden, golden])
325            }
326            Self::Squares(cells) => {
327                let side = width.min(height) / cells.max(1) as f32;
328                let along = |length: f32| -> Vec<f32> {
329                    if side <= 0.0 {
330                        return Vec::new();
331                    }
332                    (1..)
333                        .map(|i| i as f32 * side)
334                        .take_while(|at| *at < length - 0.5)
335                        .map(|at| at / length)
336                        .collect()
337                };
338                (along(width), along(height))
339            }
340        }
341    }
342}
343
344/// Draws a guide inside a box given by its corners on screen, clockwise from the top left.
345pub(super) fn paint_guide(
346    scene: &mut Scene,
347    corners: [Point; 4],
348    guide: CompositionGuide,
349    light: Color,
350    dark: Color,
351    opacity: f32,
352) {
353    let [tl, tr, br, bl] = corners;
354    let (width, height) = ((tr.x - tl.x).hypot(tr.y - tl.y), (bl.x - tl.x).hypot(bl.y - tl.y));
355    if width <= 0.0 || height <= 0.0 {
356        return;
357    }
358    let (across, down) = guide.lines(width, height);
359    let mix = |a: Point, b: Point, t: f32| Point::new(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t);
360    let (light, dark) = (light.with_alpha(light.a * opacity), dark.with_alpha(dark.a * opacity * 0.5));
361    let lines = across
362        .iter()
363        .map(|t| (mix(tl, tr, *t), mix(bl, br, *t)))
364        .chain(down.iter().map(|t| (mix(tl, bl, *t), mix(tr, br, *t))));
365    if (tl.y - tr.y).abs() < 0.01 {
366        // Upright, a line lands on whole pixels and stays sharp. The dark line beside it is what
367        // shows over a bright sky.
368        for (a, b) in lines {
369            let (x, y) = (a.x.round(), a.y.round());
370            let (w, h) = ((b.x - a.x).abs().max(1.0), (b.y - a.y).abs().max(1.0));
371            let (dx, dy) = if w <= 1.0 { (1.0, 0.0) } else { (0.0, 1.0) };
372            scene.push_quad(Quad::new(Rect::new(x + dx, y + dy, w, h), dark));
373            scene.push_quad(Quad::new(Rect::new(x, y, w, h), light));
374        }
375    } else {
376        let mut outline = Vec::new();
377        for (a, b) in lines {
378            outline.extend(stroke_outline(&[a, b], 1.0));
379        }
380        if !outline.is_empty() {
381            scene.push_path(PathFill::solid(outline, light));
382        }
383    }
384}
385
386/// The corners of an upright rectangle, clockwise from the top left.
387pub(super) fn corners_of(rect: Rect) -> [Point; 4] {
388    [
389        rect.origin,
390        Point::new(rect.max_x(), rect.y()),
391        Point::new(rect.max_x(), rect.max_y()),
392        Point::new(rect.x(), rect.max_y()),
393    ]
394}
395
396/// A composition grid over a picture: thirds by default, or the other [`CompositionGuide`]s. It
397/// takes no input, so what is underneath still gets the pointer.
398///
399/// ```
400/// use ravenrs_core::Size;
401/// use ravenrs_widgets::{CompositionGuide, ContentSpace, RuleOfThirdsGrid};
402///
403/// let grid = RuleOfThirdsGrid::new()
404///     .guide(CompositionGuide::GoldenRatio)
405///     .space(ContentSpace::Fit(Size::new(6000.0, 4000.0)))
406///     .size(300.0, 200.0);
407/// ```
408#[derive(Debug, Default)]
409#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
410pub struct RuleOfThirdsGrid {
411    frame: OverlayBox,
412    guide: CompositionGuide,
413    area: Option<Rect>,
414    color: Option<Color>,
415}
416
417#[cfg_attr(feature = "live", ravenrs_live::expose)]
418impl RuleOfThirdsGrid {
419    /// Thirds over the whole content.
420    pub fn new() -> Self {
421        Self::default()
422    }
423
424    /// Which lines it draws.
425    pub fn guide(mut self, guide: CompositionGuide) -> Self {
426        self.guide = guide;
427        self
428    }
429
430    /// Draws only inside this rectangle of the content, such as a crop.
431    pub fn area(mut self, area: Rect) -> Self {
432        self.area = Some(area);
433        self
434    }
435
436    /// The color of the lines, instead of a light line with a dark one beside it.
437    pub fn color(mut self, color: Color) -> Self {
438        self.color = Some(color);
439        self
440    }
441}
442
443overlay_builders!(RuleOfThirdsGrid, frame);
444
445impl View for RuleOfThirdsGrid {
446    fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
447        let (mut light, mut dark) = picture_inks(cx.theme());
448        if let Some(color) = self.color {
449            light = color;
450            dark = Color::TRANSPARENT;
451        }
452        self.frame.space.watch();
453        let (space, area, guide) = (self.frame.space.clone(), self.area, self.guide);
454        let paint = Paint::Custom(Rc::new(move |frame: Rect, scene: &mut Scene| {
455            let map = space.map(frame);
456            let shown = area.map_or(map.placed, |area| map.screen_rect(area));
457            scene.push_clip(frame);
458            paint_guide(scene, corners_of(shown), guide, light, dark, 0.45);
459            scene.pop_clip();
460        }));
461        let (root, surface) = self.frame.build(cx, paint);
462        // Only lines to look by: there is nothing in them to tell a screen reader.
463        cx.semantics(surface, Semantics::hidden());
464        root
465    }
466}
467
468// ---------------------------------------------------------------------------------------------
469// RubberBand
470// ---------------------------------------------------------------------------------------------
471
472/// Below this many pixels, a press and release is a click and not a selection.
473const BAND_MIN_DRAG: f32 = 4.0;
474
475#[derive(Debug, Default)]
476struct BandMemory {
477    frame: Cell<Rect>,
478    /// Where the drag began, in units of the content.
479    pressed: Cell<Option<Point>>,
480}
481
482/// A rectangle drawn with a drag, in the units of the content. The selection follows the drag
483/// through its binding, and [`on_select`](Self::on_select) gets it on release; a press and
484/// release without moving clears it. Escape cancels a drag.
485///
486/// Keyboard: the arrows move it, with Alt they resize it (and with Shift, further), Enter
487/// selects it, Delete or Escape clears it. With nothing selected, an arrow starts from the
488/// middle quarter of the content.
489///
490/// While dragging it paints without a build, so it costs no rebuilds if nothing else reads the
491/// selection.
492///
493/// ```
494/// use ravenrs_core::Rect;
495/// use ravenrs_reactive::State;
496/// use ravenrs_widgets::RubberBand;
497///
498/// let selection = State::new(None::<Rect>);
499/// let band = RubberBand::new(selection.binding())
500///     .keep(false)
501///     .on_select(|rect| println!("zoom to {rect:?}"))
502///     .size(320.0, 200.0);
503/// ```
504#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
505pub struct RubberBand {
506    selection: Binding<Option<Rect>>,
507    frame: OverlayBox,
508    on_select: Option<Rc<dyn Fn(Rect)>>,
509    keep: bool,
510    disabled: bool,
511    label: Option<String>,
512}
513
514impl fmt::Debug for RubberBand {
515    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
516        f.debug_struct("RubberBand").field("frame", &self.frame).finish_non_exhaustive()
517    }
518}
519
520#[cfg_attr(feature = "live", ravenrs_live::expose)]
521impl RubberBand {
522    /// A band bound to `selection`, in the units of its [space](Self::space).
523    pub fn new(selection: Binding<Option<Rect>>) -> Self {
524        Self {
525            selection,
526            frame: OverlayBox::default(),
527            on_select: None,
528            keep: true,
529            disabled: false,
530            label: None,
531        }
532    }
533
534    /// Runs `handler` with the rectangle when a drag ends, or on Enter.
535    pub fn on_select(mut self, handler: impl Fn(Rect) + 'static) -> Self {
536        self.on_select = Some(Rc::new(handler));
537        self
538    }
539
540    /// Whether the rectangle stays after the release (it does by default). A box for zooming,
541    /// say, goes away as soon as it is used.
542    pub fn keep(mut self, keep: bool) -> Self {
543        self.keep = keep;
544        self
545    }
546
547    /// Takes no input while disabled, which leaves the pointer to what is underneath: for a
548    /// band that only works in one mode or with a key held.
549    pub fn disabled(mut self, disabled: bool) -> Self {
550        self.disabled = disabled;
551        self
552    }
553
554    /// The name screen readers announce, instead of the one in [`Labels`](crate::Labels).
555    pub fn label(mut self, label: impl Into<String>) -> Self {
556        self.label = Some(label.into());
557        self
558    }
559}
560
561overlay_builders!(RubberBand, frame);
562
563/// `rect` moved to be inside `bounds`, and no larger than them.
564fn kept_inside(rect: Rect, bounds: Rect) -> Rect {
565    let (w, h) = (rect.width().min(bounds.width()), rect.height().min(bounds.height()));
566    Rect::new(
567        rect.x().clamp(bounds.x(), (bounds.max_x() - w).max(bounds.x())),
568        rect.y().clamp(bounds.y(), (bounds.max_y() - h).max(bounds.y())),
569        w,
570        h,
571    )
572}
573
574fn clamped(at: Point, bounds: Rect) -> Point {
575    Point::new(at.x.clamp(bounds.x(), bounds.max_x()), at.y.clamp(bounds.y(), bounds.max_y()))
576}
577
578#[cfg_attr(feature = "live", ravenrs_live::expose(part = "part1"))]
579impl RubberBand {
580    fn on_key(
581        selection: &Binding<Option<Rect>>,
582        memory: &BandMemory,
583        space: &ContentSpace,
584        on_select: Option<&Rc<dyn Fn(Rect)>>,
585        key: &Key,
586        modifiers: Modifiers,
587    ) -> bool {
588        let current = selection.get();
589        match key {
590            Key::Escape if memory.pressed.get().is_some() || current.is_some() => {
591                memory.pressed.set(None);
592                selection.set(None);
593                return true;
594            }
595            Key::Delete | Key::Backspace if current.is_some() => {
596                selection.set(None);
597                return true;
598            }
599            Key::Enter => {
600                let (Some(rect), Some(handler)) = (current, on_select) else { return false };
601                handler(rect);
602                return true;
603            }
604            _ => {}
605        }
606        let (dx, dy) = match key {
607            Key::ArrowLeft => (-1.0, 0.0),
608            Key::ArrowRight => (1.0, 0.0),
609            Key::ArrowUp => (0.0, -1.0),
610            Key::ArrowDown => (0.0, 1.0),
611            _ => return false,
612        };
613        let map = space.map(memory.frame.get());
614        let bounds = map.bounds();
615        let Some(rect) = current else {
616            let (w, h) = (bounds.width() / 2.0, bounds.height() / 2.0);
617            selection.set(Some(Rect::new(w / 2.0, h / 2.0, w, h)));
618            return true;
619        };
620        let step = (if modifiers.shift { 10.0 } else { 1.0 }) / map.scale.max(f32::EPSILON);
621        let next = if modifiers.alt {
622            let least = BAND_MIN_DRAG / map.scale.max(f32::EPSILON);
623            Rect::new(
624                rect.x(),
625                rect.y(),
626                (rect.width() + dx * step).max(least),
627                (rect.height() + dy * step).max(least),
628            )
629        } else {
630            Rect::new(rect.x() + dx * step, rect.y() + dy * step, rect.width(), rect.height())
631        };
632        selection.set(Some(kept_inside(next, bounds)));
633        true
634    }
635}
636
637impl View for RubberBand {
638    fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
639        let theme = *cx.theme();
640        let labels = cx.labels();
641        let live = cx.use_state(|| false);
642        let is_live = live.get();
643        // During a drag it paints from the binding on its own: reading it here too would make
644        // every step of the drag a rebuild.
645        let current = if is_live {
646            Observer::untracked(|| self.selection.get())
647        } else {
648            self.frame.space.watch();
649            self.selection.get()
650        };
651        let memory = cx.use_state(|| Rc::new(BandMemory::default())).get();
652        let (light, dark) = picture_inks(&theme);
653        let (space, selection, remembered) =
654            (self.frame.space.clone(), self.selection.clone(), memory.clone());
655        let paint = gesture_paint(is_live, move |frame, scene| {
656            remembered.frame.set(frame);
657            let Some(rect) = selection.get() else { return };
658            let shown = space.rect_to_screen(frame, rect);
659            scene.push_clip(frame);
660            scene.push_quad(Quad::new(shown, light.with_alpha(0.12)).border(1.0, light));
661            scene.push_quad(
662                Quad::new(shown.inflate(1.0), Color::TRANSPARENT).border(1.0, dark.with_alpha(0.55)),
663            );
664            scene.pop_clip();
665        });
666        let (root, surface) = self.frame.build(cx, paint);
667        let name = self.label.clone().unwrap_or_else(|| labels.rubber_band.clone());
668        let mut semantics = Semantics::new(Role::Group).name(name).disabled(self.disabled);
669        if let Some(rect) = current {
670            semantics = semantics.value(format!(
671                "{:.0}, {:.0}, {:.0} × {:.0}",
672                rect.x(),
673                rect.y(),
674                rect.width(),
675                rect.height()
676            ));
677        }
678        cx.semantics(surface, semantics);
679        if self.disabled {
680            return root;
681        }
682        cx.focusable(surface, 0.0);
683        cx.cursor(surface, CursorIcon::Default);
684        let (selection, space, on_select, keep, grabbed) = (
685            self.selection.clone(),
686            self.frame.space.clone(),
687            self.on_select.clone(),
688            self.keep,
689            memory.clone(),
690        );
691        cx.on_drag(
692            surface,
693            Rc::new(move |d| {
694                let map = space.map(d.frame);
695                let at = clamped(map.content(d.position), map.bounds());
696                match d.phase {
697                    DragPhase::Start => {
698                        grabbed.pressed.set(Some(at));
699                        live.set_if_changed(true);
700                    }
701                    DragPhase::Move => {
702                        let Some(pressed) = grabbed.pressed.get() else { return };
703                        let rect = spanned(pressed, at);
704                        let far = BAND_MIN_DRAG / map.scale.max(f32::EPSILON);
705                        if rect.width().max(rect.height()) >= far || selection.get().is_some() {
706                            selection.set(Some(rect));
707                        }
708                    }
709                    DragPhase::End => {
710                        live.set_if_changed(false);
711                        let Some(pressed) = grabbed.pressed.take() else { return };
712                        let rect = spanned(pressed, at);
713                        let far = BAND_MIN_DRAG / map.scale.max(f32::EPSILON);
714                        if rect.width().max(rect.height()) < far {
715                            if selection.get().is_some() {
716                                selection.set(None);
717                            }
718                            return;
719                        }
720                        if let Some(handler) = &on_select {
721                            handler(rect);
722                        }
723                        if !keep {
724                            selection.set(None);
725                        }
726                    }
727                }
728            }),
729        );
730        let (selection, space, on_select) =
731            (self.selection.clone(), self.frame.space.clone(), self.on_select.clone());
732        cx.on_key(
733            surface,
734            Rc::new(move |key, modifiers| {
735                Self::on_key(&selection, &memory, &space, on_select.as_ref(), key, modifiers)
736            }),
737        );
738        root
739    }
740}
741
742// ---------------------------------------------------------------------------------------------
743// Marquee
744// ---------------------------------------------------------------------------------------------
745
746/// The length of a dash of a marquee and of the gap after it.
747const MARQUEE_DASH: f32 = 4.0;
748/// How fast the dashes walk, in pixels per second.
749const MARQUEE_SPEED: f32 = 16.0;
750
751/// Draws straight lines, each as a sharp quad when it can be and in one path otherwise.
752struct Strokes<'a> {
753    scene: &'a mut Scene,
754    frame: Rect,
755    width: f32,
756    color: Color,
757    path: Vec<PathElement>,
758}
759
760#[cfg_attr(feature = "live", ravenrs_live::expose)]
761impl Strokes<'_> {
762    fn line(&mut self, a: Point, b: Point) {
763        let (x, y) = (a.x.min(b.x), a.y.min(b.y));
764        let (w, h) = ((b.x - a.x).abs(), (b.y - a.y).abs());
765        // A straight line on whole pixels stays sharp: half a pixel each side of one would read
766        // as a gray blur, and the dashes would vanish into the dark line under them.
767        let bounds = if h < 0.01 {
768            Rect::new(x, y.round(), w.max(1.0), 1.0)
769        } else if w < 0.01 {
770            Rect::new(x.round(), y, 1.0, h.max(1.0))
771        } else {
772            let half = self.width / 2.0;
773            if Rect::new(x, y, w, h).inflate(half).intersect(&self.frame).is_empty() {
774                return;
775            }
776            self.path.extend(stroke_outline(&[a, b], self.width));
777            return;
778        };
779        if !bounds.intersect(&self.frame).is_empty() {
780            self.scene.push_quad(Quad::new(bounds, self.color));
781        }
782    }
783
784    fn finish(self) {
785        if !self.path.is_empty() {
786            self.scene.push_path(PathFill::solid(self.path, self.color));
787        }
788    }
789}
790
791/// Calls `dash` for each dash along a line of connected points, with the pattern shifted by
792/// `phase` pixels and continuing around its corners.
793fn march(points: &[Point], phase: f32, mut dash: impl FnMut(Point, Point)) {
794    let period = MARQUEE_DASH * 2.0;
795    let mut walked = 0.0;
796    for pair in points.windows(2) {
797        let (a, b) = (pair[0], pair[1]);
798        let length = (b.x - a.x).hypot(b.y - a.y);
799        if length <= 0.0 {
800            continue;
801        }
802        let at = |t: f32| Point::new(a.x + (b.x - a.x) * t / length, a.y + (b.y - a.y) * t / length);
803        let mut t = 0.0;
804        while t < length {
805            let place = (walked + t - phase).rem_euclid(period);
806            if place < MARQUEE_DASH {
807                let end = (t + MARQUEE_DASH - place).min(length);
808                dash(at(t), at(end));
809                t = end.max(t + 0.001);
810            } else {
811                t += (period - place).max(0.001);
812            }
813        }
814        walked += length;
815    }
816}
817
818/// The animated outline of a selection, the "marching ants": a dark line with light dashes
819/// walking over it, which reads over any picture. Give it a rectangle, a closed path or loose
820/// segments, such as the edges of a mask, in the units of the [space](Self::space).
821///
822/// The dashes stop with reduced motion, and then it asks for no frames at all. It takes no
823/// input, so what is underneath still gets the pointer.
824///
825/// ```
826/// use ravenrs_core::Rect;
827/// use ravenrs_widgets::Marquee;
828///
829/// let marquee = Marquee::rect(Rect::new(20.0, 20.0, 120.0, 80.0)).size(320.0, 200.0);
830/// ```
831#[derive(Debug)]
832#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
833pub struct Marquee {
834    lines: Rc<[Vec<Point>]>,
835    frame: OverlayBox,
836    animated: bool,
837    label: Option<String>,
838}
839
840#[cfg_attr(feature = "live", ravenrs_live::expose)]
841impl Marquee {
842    /// Around a rectangle.
843    pub fn rect(rect: Rect) -> Self {
844        let [a, b, c, d] = corners_of(rect);
845        Self::from_lines(vec![vec![a, b, c, d, a]])
846    }
847
848    /// Along a closed path of these points.
849    pub fn path(points: impl IntoIterator<Item = Point>) -> Self {
850        let mut points: Vec<Point> = points.into_iter().collect();
851        if let (Some(first), true) = (points.first().copied(), points.len() > 2) {
852            points.push(first);
853        }
854        Self::from_lines(vec![points])
855    }
856
857    /// Along separate segments, in any order: the outline of a mask, say.
858    pub fn segments(segments: impl IntoIterator<Item = (Point, Point)>) -> Self {
859        Self::from_lines(segments.into_iter().map(|(a, b)| vec![a, b]).collect())
860    }
861
862    fn from_lines(lines: Vec<Vec<Point>>) -> Self {
863        Self { lines: lines.into(), frame: OverlayBox::default(), animated: true, label: None }
864    }
865
866    /// Whether the dashes walk (they do by default, unless motion is reduced).
867    pub fn animated(mut self, animated: bool) -> Self {
868        self.animated = animated;
869        self
870    }
871
872    /// The name screen readers announce, instead of the one in [`Labels`](crate::Labels).
873    pub fn label(mut self, label: impl Into<String>) -> Self {
874        self.label = Some(label.into());
875        self
876    }
877}
878
879overlay_builders!(Marquee, frame);
880
881impl View for Marquee {
882    fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
883        let (light, dark) = picture_inks(cx.theme());
884        let name = self.label.clone().unwrap_or_else(|| cx.labels().marquee.clone());
885        self.frame.space.watch();
886        let (lines, space) = (self.lines.clone(), self.frame.space.clone());
887        let draw = move |frame: Rect, phase: f32, scene: &mut Scene| {
888            let map = space.map(frame);
889            let shown: Vec<Vec<Point>> =
890                lines.iter().map(|line| line.iter().map(|p| map.screen(*p)).collect()).collect();
891            scene.push_clip(frame);
892            let mut base = Strokes { scene, frame, width: 1.5, color: dark, path: Vec::new() };
893            for line in &shown {
894                for pair in line.windows(2) {
895                    base.line(pair[0], pair[1]);
896                }
897            }
898            base.finish();
899            let mut dashes = Strokes { scene, frame, width: 1.0, color: light, path: Vec::new() };
900            for line in &shown {
901                march(line, phase, |a, b| dashes.line(a, b));
902            }
903            dashes.finish();
904            scene.pop_clip();
905        };
906        let paint = if self.animated && !cx.reduced_motion() {
907            Paint::Live(Rc::new(move |frame, time, scene| draw(frame, time as f32 * MARQUEE_SPEED, scene)))
908        } else {
909            Paint::Custom(Rc::new(move |frame, scene| draw(frame, 0.0, scene)))
910        };
911        let (root, surface) = self.frame.build(cx, paint);
912        cx.semantics(surface, Semantics::new(Role::Image).name(name));
913        root
914    }
915}
916
917// ---------------------------------------------------------------------------------------------
918// Eyedropper
919// ---------------------------------------------------------------------------------------------
920
921/// What an [`Eyedropper`] and an [`EyedropperArea`] share, and how a color reaches them: from
922/// the area, or from whoever knows the color under the pointer through [`pick`](Self::pick).
923/// Nothing here can read the screen, so the color always comes from the caller.
924#[derive(Clone)]
925#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
926pub struct EyedropperState {
927    armed: State<bool>,
928    picked: State<Option<Color>>,
929    on_pick: Option<Rc<dyn Fn(Color)>>,
930    stay_armed: bool,
931}
932
933impl fmt::Debug for EyedropperState {
934    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
935        f.debug_struct("EyedropperState").field("armed", &self.armed.get()).finish_non_exhaustive()
936    }
937}
938
939impl Default for EyedropperState {
940    fn default() -> Self {
941        Self::new()
942    }
943}
944
945#[cfg_attr(feature = "live", ravenrs_live::expose)]
946impl EyedropperState {
947    /// Not armed, with no color yet.
948    pub fn new() -> Self {
949        Self { armed: State::new(false), picked: State::new(None), on_pick: None, stay_armed: false }
950    }
951
952    /// Runs `handler` with each color picked.
953    pub fn on_pick(mut self, handler: impl Fn(Color) + 'static) -> Self {
954        self.on_pick = Some(Rc::new(handler));
955        self
956    }
957
958    /// Stays armed after a pick, to take several in a row (by default it disarms).
959    pub fn stay_armed(mut self, stay: bool) -> Self {
960        self.stay_armed = stay;
961        self
962    }
963
964    /// Whether the next click on the picture picks a color.
965    pub fn is_armed(&self) -> bool {
966        self.armed.get()
967    }
968
969    /// Arms it or disarms it.
970    pub fn set_armed(&self, armed: bool) {
971        self.armed.set_if_changed(armed);
972    }
973
974    /// The last color picked.
975    pub fn picked(&self) -> Option<Color> {
976        self.picked.get()
977    }
978
979    /// Tells it the color of the point clicked. It counts only while armed, and returns whether
980    /// it did.
981    pub fn pick(&self, color: Color) -> bool {
982        if !self.armed.get() {
983            return false;
984        }
985        self.picked.set(Some(color));
986        if !self.stay_armed {
987            self.armed.set(false);
988        }
989        if let Some(handler) = &self.on_pick {
990            handler(color);
991        }
992        true
993    }
994
995    /// While armed, Escape disarms it wherever the focus is.
996    fn listen_for_escape(&self, cx: &mut BuildCx<'_>) {
997        if self.armed.get() {
998            let armed = self.armed.clone();
999            cx.shortcut(Key::Escape, false, Rc::new(move || armed.set(false)));
1000        }
1001    }
1002}
1003
1004/// The button that arms an eyedropper, which looks pressed until a color is picked or Escape
1005/// disarms it. The color comes from an [`EyedropperArea`] or from
1006/// [`EyedropperState::pick`]: nothing here can read the screen.
1007///
1008/// ```
1009/// use ravenrs_core::{Color, Size};
1010/// use ravenrs_widgets::{ContentSpace, Eyedropper, EyedropperArea, EyedropperState};
1011///
1012/// let dropper = EyedropperState::new().on_pick(|color| println!("{color:?}"));
1013/// let button = Eyedropper::new(&dropper);
1014/// let area = EyedropperArea::new(&dropper, |at| Some(Color::rgb(at.x / 300.0, at.y / 200.0, 0.5)))
1015///     .space(ContentSpace::Fit(Size::new(300.0, 200.0)))
1016///     .size(300.0, 200.0);
1017/// ```
1018#[derive(Debug)]
1019#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
1020pub struct Eyedropper {
1021    state: EyedropperState,
1022    icon: IconName,
1023    size: ButtonSize,
1024    disabled: bool,
1025    label: Option<String>,
1026    hint: Option<String>,
1027}
1028
1029#[cfg_attr(feature = "live", ravenrs_live::expose)]
1030impl Eyedropper {
1031    /// The button of this eyedropper.
1032    pub fn new(state: &EyedropperState) -> Self {
1033        Self {
1034            state: state.clone(),
1035            icon: IconName::Crosshair,
1036            size: ButtonSize::Small,
1037            disabled: false,
1038            label: None,
1039            hint: None,
1040        }
1041    }
1042
1043    /// Its icon, instead of a crosshair.
1044    pub fn icon(mut self, icon: IconName) -> Self {
1045        self.icon = icon;
1046        self
1047    }
1048
1049    /// The size of the button.
1050    pub fn size(mut self, size: ButtonSize) -> Self {
1051        self.size = size;
1052        self
1053    }
1054
1055    /// Whether it can be armed.
1056    pub fn disabled(mut self, disabled: bool) -> Self {
1057        self.disabled = disabled;
1058        self
1059    }
1060
1061    /// The name screen readers announce, instead of the one in [`Labels`](crate::Labels).
1062    pub fn label(mut self, label: impl Into<String>) -> Self {
1063        self.label = Some(label.into());
1064        self
1065    }
1066
1067    /// What its tooltip says, instead of the one in [`Labels`](crate::Labels).
1068    pub fn hint(mut self, hint: impl Into<String>) -> Self {
1069        self.hint = Some(hint.into());
1070        self
1071    }
1072}
1073
1074impl View for Eyedropper {
1075    fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
1076        let labels = cx.labels();
1077        self.state.listen_for_escape(cx);
1078        let armed = self.state.armed.clone();
1079        let is_armed = armed.get() && !self.disabled;
1080        let button = IconButton::new(self.icon)
1081            .label(self.label.clone().unwrap_or_else(|| labels.eyedropper.clone()))
1082            .size(self.size)
1083            .disabled(self.disabled)
1084            .selected(is_armed)
1085            .on_click(move || armed.update(|a| *a = !*a));
1086        let hint = self.hint.clone().unwrap_or_else(|| labels.eyedropper_hint.clone());
1087        cx.child(&Tooltip::new(button, hint))
1088    }
1089}
1090
1091/// Where an eyedropper picks from, over a picture. While armed, a click asks `sampler` for the
1092/// color at that point of the content, in the units of the [space](Self::space); a point out of
1093/// the content or a `None` picks nothing and leaves it armed. While not armed it takes no input.
1094///
1095/// `CursorIcon` has no eyedropper, so while armed the cursor is the pointing hand.
1096#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
1097pub struct EyedropperArea {
1098    state: EyedropperState,
1099    sampler: Rc<dyn Fn(Point) -> Option<Color>>,
1100    frame: OverlayBox,
1101    label: Option<String>,
1102}
1103
1104impl fmt::Debug for EyedropperArea {
1105    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1106        f.debug_struct("EyedropperArea").field("state", &self.state).finish_non_exhaustive()
1107    }
1108}
1109
1110#[cfg_attr(feature = "live", ravenrs_live::expose)]
1111impl EyedropperArea {
1112    /// An area of this eyedropper that reads colors with `sampler`.
1113    pub fn new(state: &EyedropperState, sampler: impl Fn(Point) -> Option<Color> + 'static) -> Self {
1114        Self { state: state.clone(), sampler: Rc::new(sampler), frame: OverlayBox::default(), label: None }
1115    }
1116
1117    /// The name screen readers announce while armed, instead of the one in
1118    /// [`Labels`](crate::Labels).
1119    pub fn label(mut self, label: impl Into<String>) -> Self {
1120        self.label = Some(label.into());
1121        self
1122    }
1123}
1124
1125overlay_builders!(EyedropperArea, frame);
1126
1127impl View for EyedropperArea {
1128    fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
1129        let labels = cx.labels();
1130        self.state.listen_for_escape(cx);
1131        let (root, surface) = self.frame.build(cx, Paint::None);
1132        if !self.state.armed.get() {
1133            return root;
1134        }
1135        let name = self.label.clone().unwrap_or_else(|| labels.eyedropper_area.clone());
1136        cx.semantics(surface, Semantics::new(Role::Group).name(name));
1137        cx.cursor(surface, CursorIcon::Pointer);
1138        let (state, sampler, space) = (self.state.clone(), self.sampler.clone(), self.frame.space.clone());
1139        cx.on_drag(
1140            surface,
1141            Rc::new(move |d| {
1142                if d.phase != DragPhase::Start {
1143                    return;
1144                }
1145                let map = space.map(d.frame);
1146                if !map.placed.contains(d.position) {
1147                    return;
1148                }
1149                if let Some(color) = sampler(map.content(d.position)) {
1150                    state.pick(color);
1151                }
1152            }),
1153        );
1154        root
1155    }
1156}