Skip to main content

ravenrs_widgets/imaging/
transform.rs

1//! A box you can move, resize and turn, the crop built on it, and the band that sets an angle.
2//!
3//! The renderer has no transforms, so none of this turns a picture: these widgets draw and hand
4//! back geometry, and applying it is up to whoever owns the pixels.
5
6use std::cell::Cell;
7use std::fmt;
8use std::rc::Rc;
9
10use ravenrs_core::{
11    Color, CursorIcon, FillRule, Insets, Key, Modifiers, PathElement, PathFill, Point, Quad, Rect, Role,
12    Scene, Size,
13};
14use ravenrs_layout::{Align, Axis, Justify, LayoutStyle};
15use ravenrs_reactive::{Binding, Observer};
16use ravenrs_style::{Semantic, TextStyle};
17
18use super::overlays::{
19    CompositionGuide, ContentMap, ContentSpace, OverlayBox, corners_of, gesture_paint, overlay_builders,
20    paint_guide, picture_inks,
21};
22use super::stroke_outline;
23use crate::View;
24use crate::context::{BuildCx, DragPhase, NodeId, Paint, Semantics};
25use crate::util;
26
27// ---------------------------------------------------------------------------------------------
28// Geometry
29// ---------------------------------------------------------------------------------------------
30
31/// A rectangle turned around a point, which is what a [`TransformHandles`] edits.
32#[derive(Clone, Copy, Debug, PartialEq)]
33#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
34pub struct BoxTransform {
35    /// The box before turning, in the units of the content.
36    pub rect: Rect,
37    /// How far it is turned clockwise, in degrees.
38    pub angle: f32,
39    /// The point it turns around, as a share of the box: `(0.5, 0.5)` is its middle.
40    pub pivot: Point,
41}
42
43#[cfg_attr(feature = "live", ravenrs_live::expose)]
44impl BoxTransform {
45    /// An upright box that turns around its middle.
46    pub const fn new(rect: Rect) -> Self {
47        Self { rect, angle: 0.0, pivot: Point::new(0.5, 0.5) }
48    }
49
50    /// The same box turned clockwise by this many degrees.
51    pub const fn rotated(mut self, angle: f32) -> Self {
52        self.angle = angle;
53        self
54    }
55
56    /// The point it turns around, in the units of the content.
57    pub fn pivot_point(&self) -> Point {
58        self.at_share(self.pivot.x, self.pivot.y)
59    }
60
61    fn at_share(&self, x: f32, y: f32) -> Point {
62        Point::new(self.rect.x() + self.rect.width() * x, self.rect.y() + self.rect.height() * y)
63    }
64
65    /// Where a point of the upright box ends up once turned.
66    pub fn to_world(&self, local: Point) -> Point {
67        let p = self.pivot_point();
68        let (sin, cos) = self.angle.to_radians().sin_cos();
69        let (x, y) = (local.x - p.x, local.y - p.y);
70        Point::new(p.x + x * cos - y * sin, p.y + x * sin + y * cos)
71    }
72
73    /// The point of the upright box that ends up at `world` once turned.
74    pub fn to_local(&self, world: Point) -> Point {
75        let p = self.pivot_point();
76        let (sin, cos) = self.angle.to_radians().sin_cos();
77        let (x, y) = (world.x - p.x, world.y - p.y);
78        Point::new(p.x + x * cos + y * sin, p.y - x * sin + y * cos)
79    }
80
81    /// The four corners once turned, clockwise from what was the top left.
82    pub fn corners(&self) -> [Point; 4] {
83        corners_of(self.rect).map(|p| self.to_world(p))
84    }
85
86    /// Where a handle is once turned.
87    pub fn handle(&self, handle: BoxHandle) -> Point {
88        let (x, y) = handle.share();
89        self.to_world(self.at_share(x, y))
90    }
91
92    fn is_inside(&self, bounds: Rect) -> bool {
93        const SLACK: f32 = 0.01;
94        self.corners().iter().all(|c| {
95            c.x >= bounds.x() - SLACK
96                && c.x <= bounds.max_x() + SLACK
97                && c.y >= bounds.y() - SLACK
98                && c.y <= bounds.max_y() + SLACK
99        })
100    }
101
102    /// Moved the least it takes to be inside `bounds`.
103    fn moved_inside(mut self, bounds: Rect) -> Self {
104        let corners = self.corners();
105        let low = |pick: fn(&Point) -> f32| corners.iter().map(pick).fold(f32::INFINITY, f32::min);
106        let high = |pick: fn(&Point) -> f32| corners.iter().map(pick).fold(f32::NEG_INFINITY, f32::max);
107        let axis = |low: f32, high: f32, from: f32, to: f32| {
108            if high - low > to - from {
109                (from + to - low - high) / 2.0
110            } else if low < from {
111                from - low
112            } else if high > to {
113                to - high
114            } else {
115                0.0
116            }
117        };
118        let dx = axis(low(|p| p.x), high(|p| p.x), bounds.x(), bounds.max_x());
119        let dy = axis(low(|p| p.y), high(|p| p.y), bounds.y(), bounds.max_y());
120        self.rect = self.rect.translate(Point::new(dx, dy));
121        self
122    }
123
124    /// Made smaller around its middle until it is inside `bounds`, for a box that turns around
125    /// its middle. A crop does this as it is turned, the way a frame must over a still picture.
126    fn shrunk_inside(mut self, bounds: Rect) -> Self {
127        let c = self.rect.center();
128        let mut share = 1.0_f32;
129        for corner in self.corners() {
130            let (dx, dy) = (corner.x - c.x, corner.y - c.y);
131            if dx > 0.0 {
132                share = share.min((bounds.max_x() - c.x) / dx);
133            } else if dx < 0.0 {
134                share = share.min((bounds.x() - c.x) / dx);
135            }
136            if dy > 0.0 {
137                share = share.min((bounds.max_y() - c.y) / dy);
138            } else if dy < 0.0 {
139                share = share.min((bounds.y() - c.y) / dy);
140            }
141        }
142        let share = share.clamp(0.0, 1.0);
143        let (w, h) = (self.rect.width() * share, self.rect.height() * share);
144        self.rect = Rect::new(c.x - w / 2.0, c.y - h / 2.0, w, h);
145        self
146    }
147}
148
149/// One of the eight handles of a [`TransformHandles`].
150#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
151#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
152pub enum BoxHandle {
153    /// The top left corner.
154    TopLeft,
155    /// The middle of the top side.
156    Top,
157    /// The top right corner.
158    TopRight,
159    /// The middle of the right side.
160    Right,
161    /// The bottom right corner.
162    BottomRight,
163    /// The middle of the bottom side.
164    Bottom,
165    /// The bottom left corner.
166    BottomLeft,
167    /// The middle of the left side.
168    Left,
169}
170
171#[cfg_attr(feature = "live", ravenrs_live::expose)]
172impl BoxHandle {
173    /// All of them, clockwise from the top left.
174    pub const ALL: [Self; 8] = [
175        Self::TopLeft,
176        Self::Top,
177        Self::TopRight,
178        Self::Right,
179        Self::BottomRight,
180        Self::Bottom,
181        Self::BottomLeft,
182        Self::Left,
183    ];
184
185    /// Where it sits, as a share of the box.
186    pub const fn share(self) -> (f32, f32) {
187        match self {
188            Self::TopLeft => (0.0, 0.0),
189            Self::Top => (0.5, 0.0),
190            Self::TopRight => (1.0, 0.0),
191            Self::Right => (1.0, 0.5),
192            Self::BottomRight => (1.0, 1.0),
193            Self::Bottom => (0.5, 1.0),
194            Self::BottomLeft => (0.0, 1.0),
195            Self::Left => (0.0, 0.5),
196        }
197    }
198
199    const fn is_corner(self) -> bool {
200        matches!(self, Self::TopLeft | Self::TopRight | Self::BottomRight | Self::BottomLeft)
201    }
202}
203
204/// How a box answers a resize.
205#[derive(Clone, Copy, Debug)]
206struct Resize {
207    handle: BoxHandle,
208    /// Width over height to hold.
209    ratio: Option<f32>,
210    around_pivot: bool,
211    least: Size,
212}
213
214#[cfg_attr(feature = "live", ravenrs_live::expose)]
215impl Resize {
216    /// `start` with its handle moved by this much along the box's own axes. The opposite handle,
217    /// or the pivot, stays where it was on screen.
218    fn apply(&self, start: BoxTransform, du: f32, dv: f32) -> BoxTransform {
219        let (fx, fy) = self.handle.share();
220        let (sx, sy) = (fx * 2.0 - 1.0, fy * 2.0 - 1.0);
221        let (ax, ay) = if self.around_pivot { (start.pivot.x, start.pivot.y) } else { (1.0 - fx, 1.0 - fy) };
222        // Around a pivot a handle moves less than the side does, in proportion to how far apart
223        // they are. A pivot on the handle would make that endless.
224        let reach = |at: f32, from: f32| (at - from).abs().max(0.1);
225        let (w0, h0) = (start.rect.width(), start.rect.height());
226        let mut w = w0 + sx * du / reach(fx, ax);
227        let mut h = h0 + sy * dv / reach(fy, ay);
228        if let Some(ratio) = self.ratio {
229            if self.handle.is_corner() {
230                let grow = (w / w0.max(f32::EPSILON)).max(h / h0.max(f32::EPSILON));
231                w = w0 * grow;
232                h = w / ratio;
233            } else if sx != 0.0 {
234                h = w / ratio;
235            } else {
236                w = h * ratio;
237            }
238            let hold = (self.least.width / w.max(f32::EPSILON)).max(self.least.height / h.max(f32::EPSILON));
239            if hold > 1.0 {
240                w *= hold;
241                h *= hold;
242            }
243        } else {
244            w = w.max(self.least.width);
245            h = h.max(self.least.height);
246        }
247        // Where the fixed point must stay, and the rectangle that puts it there at the new size.
248        let fixed = start.to_world(start.at_share(ax, ay));
249        let (sin, cos) = start.angle.to_radians().sin_cos();
250        let (x, y) = ((ax - start.pivot.x) * w, (ay - start.pivot.y) * h);
251        let pivot = Point::new(fixed.x - (x * cos - y * sin), fixed.y - (x * sin + y * cos));
252        BoxTransform {
253            rect: Rect::new(pivot.x - start.pivot.x * w, pivot.y - start.pivot.y * h, w, h),
254            ..start
255        }
256    }
257
258    /// Like [`Self::apply`], as far as it goes while inside `bounds`. Without a ratio each axis
259    /// stops on its own, so a handle slides along an edge it reached.
260    fn confined(&self, start: BoxTransform, du: f32, dv: f32, bounds: Rect) -> BoxTransform {
261        let wanted = self.apply(start, du, dv);
262        if wanted.is_inside(bounds) || !start.is_inside(bounds) {
263            return wanted;
264        }
265        let furthest = |try_at: &dyn Fn(f32) -> BoxTransform| {
266            let (mut inside, mut outside) = (0.0_f32, 1.0_f32);
267            if try_at(1.0).is_inside(bounds) {
268                return 1.0;
269            }
270            for _ in 0..14 {
271                let middle = (inside + outside) / 2.0;
272                if try_at(middle).is_inside(bounds) {
273                    inside = middle;
274                } else {
275                    outside = middle;
276                }
277            }
278            inside
279        };
280        if self.ratio.is_some() {
281            let t = furthest(&|t| self.apply(start, du * t, dv * t));
282            return self.apply(start, du * t, dv * t).moved_inside(bounds);
283        }
284        let across = du * furthest(&|t| self.apply(start, du * t, 0.0));
285        let down = dv * furthest(&|t| self.apply(start, across, dv * t));
286        // The search stops a hair from the edge, on either side of it.
287        self.apply(start, across, down).moved_inside(bounds)
288    }
289}
290
291/// An angle in the range `-180..=180`.
292fn wrapped(angle: f32) -> f32 {
293    let turned = (angle + 180.0).rem_euclid(360.0) - 180.0;
294    if turned == -180.0 { 180.0 } else { turned }
295}
296
297// ---------------------------------------------------------------------------------------------
298// TransformHandles
299// ---------------------------------------------------------------------------------------------
300
301/// How far from a handle a press still takes it, in pixels: more than it looks, or pressing just
302/// off it would do something else.
303const HANDLE_REACH: f32 = 11.0;
304/// How far from a side a press takes that side.
305const EDGE_REACH: f32 = 5.0;
306const HANDLE_SIDE: f32 = 8.0;
307/// How far above the box the handle that turns it is.
308const KNOB_STEM: f32 = 24.0;
309const ROTATE_SNAP: f32 = 15.0;
310
311#[derive(Clone, Copy, Debug, PartialEq)]
312enum Part {
313    None,
314    Handle(BoxHandle),
315    Rotate,
316    Pivot,
317    Move,
318}
319
320#[derive(Clone, Copy, Debug)]
321struct Rules {
322    keep_corners: bool,
323    aspect: Option<f32>,
324    confined: bool,
325    min_size: f32,
326    knob: bool,
327    pivot: bool,
328    outside_rotates: bool,
329    angle_limit: Option<f32>,
330    /// Turning makes it smaller instead of stopping at the bounds.
331    shrink_to_turn: bool,
332}
333
334#[derive(Clone, Copy, Debug)]
335struct Look {
336    veil: bool,
337    guide: Option<CompositionGuide>,
338    turning_guide: Option<CompositionGuide>,
339}
340
341#[derive(Clone, Copy, Debug)]
342struct Grab {
343    part: Part,
344    pressed: Point,
345    start: BoxTransform,
346}
347
348#[derive(Debug, Default)]
349struct TransformMemory {
350    frame: Cell<Rect>,
351    pointer: Cell<Option<Point>>,
352    grab: Cell<Option<Grab>>,
353}
354
355/// Where the handle that turns the box is, on screen.
356fn knob_at(t: &BoxTransform, map: &ContentMap) -> Point {
357    let top = map.screen(t.handle(BoxHandle::Top));
358    let (sin, cos) = t.angle.to_radians().sin_cos();
359    Point::new(top.x + sin * KNOB_STEM, top.y - cos * KNOB_STEM)
360}
361
362fn part_at(t: &BoxTransform, map: &ContentMap, at: Point, rules: Rules) -> Part {
363    let near = |p: Point, reach: f32| (p.x - at.x).hypot(p.y - at.y) <= reach;
364    if rules.pivot && near(map.screen(t.pivot_point()), HANDLE_REACH) {
365        return Part::Pivot;
366    }
367    // The corners first: on a small box they win over the middles.
368    for handle in BoxHandle::ALL
369        .into_iter()
370        .filter(|h| h.is_corner())
371        .chain(BoxHandle::ALL.into_iter().filter(|h| !h.is_corner()))
372    {
373        if near(map.screen(t.handle(handle)), HANDLE_REACH) {
374            return Part::Handle(handle);
375        }
376    }
377    if rules.knob && near(knob_at(t, map), HANDLE_REACH) {
378        return Part::Rotate;
379    }
380    let local = t.to_local(map.content(at));
381    let reach = EDGE_REACH / map.scale.max(f32::EPSILON);
382    let rect = t.rect;
383    let across = local.x >= rect.x() - reach && local.x <= rect.max_x() + reach;
384    let along = local.y >= rect.y() - reach && local.y <= rect.max_y() + reach;
385    if across && (local.y - rect.y()).abs() <= reach {
386        return Part::Handle(BoxHandle::Top);
387    }
388    if across && (local.y - rect.max_y()).abs() <= reach {
389        return Part::Handle(BoxHandle::Bottom);
390    }
391    if along && (local.x - rect.x()).abs() <= reach {
392        return Part::Handle(BoxHandle::Left);
393    }
394    if along && (local.x - rect.max_x()).abs() <= reach {
395        return Part::Handle(BoxHandle::Right);
396    }
397    if rect.contains(local) {
398        Part::Move
399    } else if rules.outside_rotates {
400        Part::Rotate
401    } else {
402        Part::None
403    }
404}
405
406/// `CursorIcon` has no diagonal resize and no grab: sides turn as far as the two resize cursors
407/// can show, and everything else that acts on a press uses the pointing hand.
408fn cursor_of(part: Part, angle: f32) -> CursorIcon {
409    match part {
410        Part::None => CursorIcon::Default,
411        Part::Handle(handle) if !handle.is_corner() => {
412            let (sin, cos) = angle.to_radians().sin_cos();
413            let sideways = matches!(handle, BoxHandle::Left | BoxHandle::Right);
414            if (if sideways { cos } else { sin }).abs() >= std::f32::consts::FRAC_1_SQRT_2 {
415                CursorIcon::ColResize
416            } else {
417                CursorIcon::RowResize
418            }
419        }
420        Part::Handle(_) | Part::Rotate | Part::Pivot | Part::Move => CursorIcon::Pointer,
421    }
422}
423
424#[cfg_attr(feature = "live", ravenrs_live::expose(part = "part1"))]
425impl Rules {
426    fn resize(&self, handle: BoxHandle, start: BoxTransform, scale: f32, modifiers: Modifiers) -> Resize {
427        let least = self.min_size / scale.max(f32::EPSILON);
428        let held = (self.keep_corners && handle.is_corner() && !modifiers.shift)
429            .then(|| start.rect.width() / start.rect.height().max(f32::EPSILON));
430        Resize {
431            handle,
432            ratio: self.aspect.or(held),
433            around_pivot: modifiers.alt,
434            least: Size::new(least, least),
435        }
436    }
437
438    fn resized(
439        &self,
440        handle: BoxHandle,
441        start: BoxTransform,
442        du: f32,
443        dv: f32,
444        map: &ContentMap,
445        modifiers: Modifiers,
446    ) -> BoxTransform {
447        let resize = self.resize(handle, start, map.scale, modifiers);
448        if self.confined { resize.confined(start, du, dv, map.bounds()) } else { resize.apply(start, du, dv) }
449    }
450
451    fn moved(&self, start: BoxTransform, dx: f32, dy: f32, map: &ContentMap) -> BoxTransform {
452        let t = BoxTransform { rect: start.rect.translate(Point::new(dx, dy)), ..start };
453        if self.confined { t.moved_inside(map.bounds()) } else { t }
454    }
455
456    fn turned(&self, start: BoxTransform, angle: f32, map: &ContentMap) -> BoxTransform {
457        let limit = self.angle_limit.unwrap_or(180.0);
458        let t = BoxTransform { angle: wrapped(angle).clamp(-limit, limit), ..start };
459        if !self.confined || t.is_inside(map.bounds()) {
460            return t;
461        }
462        if self.shrink_to_turn {
463            return self.made_to_fit(t, map.bounds(), map.scale);
464        }
465        let (mut inside, mut outside) = (0.0_f32, 1.0_f32);
466        for _ in 0..14 {
467            let middle = (inside + outside) / 2.0;
468            let tried = BoxTransform { angle: start.angle + (t.angle - start.angle) * middle, ..start };
469            if tried.is_inside(map.bounds()) {
470                inside = middle;
471            } else {
472                outside = middle;
473            }
474        }
475        BoxTransform { angle: start.angle + (t.angle - start.angle) * inside, ..start }
476    }
477
478    /// Made smaller until it fits, and moved first if that alone would leave almost nothing: a
479    /// crop against the edge of the content would otherwise vanish as it turns.
480    fn made_to_fit(&self, t: BoxTransform, bounds: Rect, scale: f32) -> BoxTransform {
481        let least = self.min_size / scale.max(f32::EPSILON);
482        let shrunk = t.shrunk_inside(bounds);
483        if shrunk.rect.width() >= least && shrunk.rect.height() >= least {
484            return shrunk;
485        }
486        t.moved_inside(bounds).shrunk_inside(bounds)
487    }
488
489    /// What a value set from outside becomes under these rules, for a crop whose proportion or
490    /// angle was changed by another control.
491    fn settled(&self, t: BoxTransform, bounds: Rect, scale: f32) -> BoxTransform {
492        let limit = self.angle_limit.unwrap_or(180.0);
493        let mut next = BoxTransform { angle: t.angle.clamp(-limit, limit), ..t };
494        if let Some(ratio) = self.aspect
495            && next.rect.height() > 0.0
496            && (next.rect.width() / next.rect.height() - ratio).abs() > ratio * 0.005
497        {
498            let w = next.rect.width().min(next.rect.height() * ratio);
499            let h = w / ratio;
500            let c = next.rect.center();
501            next.rect = Rect::new(c.x - w / 2.0, c.y - h / 2.0, w, h);
502        }
503        if next.is_inside(bounds) { next } else { self.made_to_fit(next, bounds, scale) }
504    }
505
506    /// What a drag has made of the box, from where it began.
507    fn dragged(&self, grab: Grab, at: Point, map: &ContentMap, modifiers: Modifiers) -> BoxTransform {
508        let start = grab.start;
509        let (dx, dy) = (at.x - grab.pressed.x, at.y - grab.pressed.y);
510        match grab.part {
511            Part::None => start,
512            Part::Move => self.moved(start, dx, dy, map),
513            Part::Handle(handle) => {
514                let (sin, cos) = start.angle.to_radians().sin_cos();
515                self.resized(handle, start, dx * cos + dy * sin, -dx * sin + dy * cos, map, modifiers)
516            }
517            Part::Rotate => {
518                let p = start.pivot_point();
519                let toward = |q: Point| (q.y - p.y).atan2(q.x - p.x).to_degrees();
520                let angle = start.angle + toward(at) - toward(grab.pressed);
521                let angle = if modifiers.shift { (angle / ROTATE_SNAP).round() * ROTATE_SNAP } else { angle };
522                self.turned(start, angle, map)
523            }
524            Part::Pivot => {
525                // The box must stay where it is on screen with its turning point somewhere else,
526                // which moves the upright rectangle it is told from.
527                let local = start.to_local(at);
528                let (w, h) = (start.rect.width().max(f32::EPSILON), start.rect.height().max(f32::EPSILON));
529                let pivot = Point::new((local.x - start.rect.x()) / w, (local.y - start.rect.y()) / h);
530                let rect = Rect::new(at.x - pivot.x * w, at.y - pivot.y * h, w, h);
531                BoxTransform { rect, pivot, ..start }
532            }
533        }
534    }
535
536    fn on_key(
537        &self,
538        t: BoxTransform,
539        key: &Key,
540        modifiers: Modifiers,
541        map: &ContentMap,
542    ) -> Option<BoxTransform> {
543        let step = (if modifiers.shift { 10.0 } else { 1.0 }) / map.scale.max(f32::EPSILON);
544        let (dx, dy) = match key {
545            Key::ArrowLeft => (-step, 0.0),
546            Key::ArrowRight => (step, 0.0),
547            Key::ArrowUp => (0.0, -step),
548            Key::ArrowDown => (0.0, step),
549            Key::Character(c)
550                if (self.knob || self.outside_rotates) && matches!(c.as_str(), "[" | "]" | "{" | "}") =>
551            {
552                let turn = if modifiers.shift { ROTATE_SNAP } else { 1.0 };
553                let turn = if matches!(c.as_str(), "[" | "{") { -turn } else { turn };
554                return Some(self.turned(t, t.angle + turn, map));
555            }
556            _ => return None,
557        };
558        if !modifiers.alt {
559            return Some(self.moved(t, dx, dy, map));
560        }
561        let handle = if dx != 0.0 { BoxHandle::Right } else { BoxHandle::Bottom };
562        // The keys say how large, always from the side they name: Alt here is not around the pivot.
563        Some(self.resized(handle, t, dx, dy, map, Modifiers::default()))
564    }
565}
566
567/// Eight handles around a box, one that turns it and the point it turns around, drawn over
568/// content in the units of a [`ContentSpace`]. It edits a [`BoxTransform`] and draws nothing but
569/// the box: the renderer cannot turn a picture, so applying the geometry is up to whoever owns it.
570///
571/// A corner keeps the proportions and Shift lets go of them, Alt resizes around the pivot, and
572/// Shift holds a turn to steps of 15 degrees. Escape cancels a drag, and a double click on the
573/// pivot puts it back in the middle.
574///
575/// Keyboard: the arrows move it, with Alt they resize it, `[` and `]` turn it, and Shift makes
576/// each of these go further.
577///
578/// `CursorIcon` has no diagonal cursors, so the corners show the pointing hand. While dragging
579/// it paints without a build, so it costs no rebuilds if nothing else reads the value.
580///
581/// ```
582/// use ravenrs_core::{Rect, Size};
583/// use ravenrs_reactive::State;
584/// use ravenrs_widgets::{BoxTransform, ContentSpace, TransformHandles};
585///
586/// let layer = State::new(BoxTransform::new(Rect::new(100.0, 80.0, 300.0, 200.0)).rotated(10.0));
587/// let handles = TransformHandles::new(layer.binding())
588///     .space(ContentSpace::Fit(Size::new(1200.0, 800.0)))
589///     .size(600.0, 400.0);
590/// ```
591#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
592pub struct TransformHandles {
593    value: Binding<BoxTransform>,
594    frame: OverlayBox,
595    rules: Rules,
596    look: Look,
597    on_commit: Option<Rc<dyn Fn(BoxTransform)>>,
598    disabled: bool,
599    label: Option<String>,
600    crop_name: bool,
601}
602
603impl fmt::Debug for TransformHandles {
604    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
605        f.debug_struct("TransformHandles").field("frame", &self.frame).finish_non_exhaustive()
606    }
607}
608
609#[cfg_attr(feature = "live", ravenrs_live::expose)]
610impl TransformHandles {
611    /// Handles bound to `value`, in the units of its [space](Self::space).
612    pub fn new(value: Binding<BoxTransform>) -> Self {
613        Self {
614            value,
615            frame: OverlayBox::default(),
616            rules: Rules {
617                keep_corners: true,
618                aspect: None,
619                confined: false,
620                min_size: 12.0,
621                knob: true,
622                pivot: true,
623                outside_rotates: false,
624                angle_limit: None,
625                shrink_to_turn: false,
626            },
627            look: Look { veil: false, guide: None, turning_guide: None },
628            on_commit: None,
629            disabled: false,
630            label: None,
631            crop_name: false,
632        }
633    }
634
635    /// Whether the corners keep the proportions unless Shift is held (they do by default).
636    pub fn keep_proportions(mut self, keep: bool) -> Self {
637        self.rules.keep_corners = keep;
638        self
639    }
640
641    /// Holds this width over height for every handle.
642    pub fn aspect(mut self, aspect: Option<f32>) -> Self {
643        self.rules.aspect = aspect;
644        self
645    }
646
647    /// Keeps the whole box inside the content (it may leave by default).
648    pub fn confined(mut self, confined: bool) -> Self {
649        self.rules.confined = confined;
650        self
651    }
652
653    /// The smallest side, in logical pixels on screen at the current zoom (12 by default).
654    pub fn min_size(mut self, pixels: f32) -> Self {
655        self.rules.min_size = pixels.max(1.0);
656        self
657    }
658
659    /// Whether it can be turned by the handle above it (it can by default).
660    pub fn rotatable(mut self, rotatable: bool) -> Self {
661        self.rules.knob = rotatable;
662        self
663    }
664
665    /// Whether a drag outside the box turns it as well (it does not by default).
666    pub fn rotate_outside(mut self, rotate: bool) -> Self {
667        self.rules.outside_rotates = rotate;
668        self
669    }
670
671    /// Whether the point it turns around shows and can be moved (it does by default).
672    pub fn pivot_handle(mut self, show: bool) -> Self {
673        self.rules.pivot = show;
674        self
675    }
676
677    /// Runs `handler` with the result when a drag ends or a key changes it: one step to undo.
678    pub fn on_commit(mut self, handler: impl Fn(BoxTransform) + 'static) -> Self {
679        self.on_commit = Some(Rc::new(handler));
680        self
681    }
682
683    /// Takes no input while disabled, which leaves the pointer to what is underneath.
684    pub fn disabled(mut self, disabled: bool) -> Self {
685        self.disabled = disabled;
686        self
687    }
688
689    /// The name screen readers announce, instead of the one in [`Labels`](crate::Labels).
690    pub fn label(mut self, label: impl Into<String>) -> Self {
691        self.label = Some(label.into());
692        self
693    }
694}
695
696overlay_builders!(TransformHandles, frame);
697
698fn square(center: Point, side: f32, angle: f32) -> Vec<PathElement> {
699    let half = side / 2.0;
700    let (sin, cos) = angle.to_radians().sin_cos();
701    let corner = |x: f32, y: f32| Point::new(center.x + x * cos - y * sin, center.y + x * sin + y * cos);
702    vec![
703        PathElement::MoveTo(corner(-half, -half)),
704        PathElement::LineTo(corner(half, -half)),
705        PathElement::LineTo(corner(half, half)),
706        PathElement::LineTo(corner(-half, half)),
707        PathElement::Close,
708    ]
709}
710
711/// What a box looks like, apart from where it is.
712#[derive(Clone, Copy, Debug)]
713struct BoxStyle {
714    rules: Rules,
715    look: Look,
716    light: Color,
717    dark: Color,
718    scrim: Color,
719}
720
721fn paint_box(
722    scene: &mut Scene,
723    frame: Rect,
724    map: &ContentMap,
725    t: &BoxTransform,
726    style: BoxStyle,
727    turning: bool,
728) {
729    let BoxStyle { rules, look, light, dark, scrim } = style;
730    let corners = t.corners().map(|p| map.screen(p));
731    let upright = t.angle.to_radians().sin().abs() < 0.001;
732    let shown = map.screen_rect(t.rect);
733    scene.push_clip(frame);
734    if look.veil {
735        let area = map.placed.intersect(&frame);
736        let [a, b, c, d] = corners_of(area);
737        let mut outline = vec![
738            PathElement::MoveTo(a),
739            PathElement::LineTo(b),
740            PathElement::LineTo(c),
741            PathElement::LineTo(d),
742            PathElement::Close,
743        ];
744        outline.push(PathElement::MoveTo(corners[0]));
745        outline.extend(corners[1..].iter().map(|p| PathElement::LineTo(*p)));
746        outline.push(PathElement::Close);
747        scene.push_path(PathFill { rule: FillRule::EvenOdd, ..PathFill::solid(outline, scrim) });
748    }
749    let guide = if turning { look.turning_guide.or(look.guide) } else { look.guide };
750    if let Some(guide) = guide {
751        paint_guide(scene, corners, guide, light, dark, if turning { 0.35 } else { 0.45 });
752    }
753    if upright {
754        scene.push_quad(Quad::new(shown.inflate(1.0), Color::TRANSPARENT).border(1.0, dark.with_alpha(0.55)));
755        scene.push_quad(Quad::new(shown, Color::TRANSPARENT).border(1.0, light));
756    } else {
757        let edges = |width: f32| {
758            let mut outline = Vec::with_capacity(32);
759            for i in 0..4 {
760                outline.extend(stroke_outline(&[corners[i], corners[(i + 1) % 4]], width));
761            }
762            outline
763        };
764        scene.push_path(PathFill::solid(edges(3.0), dark.with_alpha(0.55)));
765        scene.push_path(PathFill::solid(edges(1.0), light));
766    }
767    if rules.knob {
768        let top = map.screen(t.handle(BoxHandle::Top));
769        let knob = knob_at(t, map);
770        scene.push_path(PathFill::solid(stroke_outline(&[top, knob], 3.0), dark.with_alpha(0.55)));
771        scene.push_path(PathFill::solid(stroke_outline(&[top, knob], 1.0), light));
772        let dot = Rect::new(knob.x - 5.0, knob.y - 5.0, 10.0, 10.0);
773        scene.push_quad(Quad::new(dot, light).radius(5.0).border(1.0, dark.with_alpha(0.6)));
774    }
775    for handle in BoxHandle::ALL {
776        let at = map.screen(t.handle(handle));
777        if upright {
778            let half = HANDLE_SIDE / 2.0;
779            let mark = Rect::new(at.x - half, at.y - half, HANDLE_SIDE, HANDLE_SIDE);
780            scene.push_quad(Quad::new(mark, light).radius(2.0).border(1.0, dark.with_alpha(0.6)));
781        } else {
782            scene.push_path(PathFill::solid(square(at, HANDLE_SIDE + 2.0, t.angle), dark.with_alpha(0.6)));
783            scene.push_path(PathFill::solid(square(at, HANDLE_SIDE, t.angle), light));
784        }
785    }
786    if rules.pivot {
787        let at = map.screen(t.pivot_point());
788        let ring = Rect::new(at.x - 5.0, at.y - 5.0, 10.0, 10.0);
789        scene.push_quad(
790            Quad::new(ring.inflate(1.0), Color::TRANSPARENT).radius(6.0).border(1.0, dark.with_alpha(0.6)),
791        );
792        scene.push_quad(Quad::new(ring, Color::TRANSPARENT).radius(5.0).border(1.5, light));
793        scene.push_quad(Quad::new(Rect::new(at.x - 1.0, at.y - 1.0, 2.0, 2.0), light).radius(1.0));
794    }
795    scene.pop_clip();
796}
797
798impl View for TransformHandles {
799    fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
800        let theme = *cx.theme();
801        let labels = cx.labels();
802        let live = cx.use_state(|| false);
803        let is_live = live.get();
804        // During a drag it paints from the binding on its own: reading it here too would make
805        // every step of the drag a rebuild.
806        let current = if is_live {
807            Observer::untracked(|| self.value.get())
808        } else {
809            self.frame.space.watch();
810            self.value.get()
811        };
812        let hover = cx.use_state(|| Part::None);
813        let memory = cx.use_state(|| Rc::new(TransformMemory::default())).get();
814        // Only a crop answers for what others set, since a box is otherwise free to go anywhere.
815        let current = match self.frame.known_size(cx) {
816            Some(size) if self.rules.shrink_to_turn && !is_live && size.width > 0.0 => {
817                let map = self.frame.space.map(Rect::new(0.0, 0.0, size.width, size.height));
818                let settled = self.rules.settled(current, map.bounds(), map.scale);
819                if settled != current {
820                    self.value.set(settled);
821                }
822                settled
823            }
824            _ => current,
825        };
826        let (light, dark) = picture_inks(&theme);
827        let rules = self.rules;
828        let style = BoxStyle { rules, look: self.look, light, dark, scrim: theme.color(Semantic::Scrim) };
829        let (space, value, remembered) = (self.frame.space.clone(), self.value.clone(), memory.clone());
830        let paint = gesture_paint(is_live, move |frame, scene| {
831            remembered.frame.set(frame);
832            let turning = remembered.grab.get().is_some_and(|g| g.part == Part::Rotate);
833            paint_box(scene, frame, &space.map(frame), &value.get(), style, turning);
834        });
835        let (root, surface) = self.frame.build(cx, paint);
836        let name = self
837            .label
838            .clone()
839            .unwrap_or_else(|| if self.crop_name { labels.crop.clone() } else { labels.transform.clone() });
840        let decimal = labels.decimal_separator;
841        let text = format!(
842            "{:.0}, {:.0}, {:.0} × {:.0}, {}",
843            current.rect.x(),
844            current.rect.y(),
845            current.rect.width(),
846            current.rect.height(),
847            angle_text(current.angle, decimal),
848        );
849        cx.semantics(surface, Semantics::new(Role::Group).name(name).value(text).disabled(self.disabled));
850        if self.disabled {
851            return root;
852        }
853        cx.focusable(surface, 0.0);
854        cx.cursor(surface, cursor_of(hover.get(), current.angle));
855        let modifiers = cx.modifiers();
856        let (value, space, grabbed, commit) =
857            (self.value.clone(), self.frame.space.clone(), memory.clone(), self.on_commit.clone());
858        let held = modifiers.clone();
859        let dragged_live = live.clone();
860        cx.on_drag(
861            surface,
862            Rc::new(move |d| {
863                let map = space.map(d.frame);
864                let at = map.content(d.position);
865                match d.phase {
866                    DragPhase::Start => {
867                        let start = value.get();
868                        let part = part_at(&start, &map, d.position, rules);
869                        if part != Part::None {
870                            grabbed.grab.set(Some(Grab { part, pressed: at, start }));
871                            dragged_live.set_if_changed(true);
872                        }
873                    }
874                    DragPhase::Move => {
875                        let Some(grab) = grabbed.grab.get() else { return };
876                        let next = rules.dragged(grab, at, &map, held.get());
877                        if next != value.get() {
878                            value.set(next);
879                        }
880                    }
881                    DragPhase::End => {
882                        dragged_live.set_if_changed(false);
883                        let Some(grab) = grabbed.grab.take() else { return };
884                        if let Some(commit) = &commit
885                            && value.get() != grab.start
886                        {
887                            commit(value.get());
888                        }
889                    }
890                }
891            }),
892        );
893        let (value, space, moved, hovered) =
894            (self.value.clone(), self.frame.space.clone(), memory.clone(), hover.clone());
895        cx.on_pointer_move(
896            surface,
897            Rc::new(move |at, frame| {
898                moved.pointer.set(Some(at));
899                if moved.grab.get().is_none() {
900                    hovered.set_if_changed(part_at(&value.get(), &space.map(frame), at, rules));
901                }
902            }),
903        );
904        let (value, space, clicked) = (self.value.clone(), self.frame.space.clone(), memory.clone());
905        cx.on_double_click(
906            surface,
907            Rc::new(move || {
908                let Some(at) = clicked.pointer.get() else { return };
909                let map = space.map(clicked.frame.get());
910                let t = value.get();
911                if part_at(&t, &map, at, rules) != Part::Pivot {
912                    return;
913                }
914                let middle = t.rect.center();
915                let grab = Grab { part: Part::Pivot, pressed: middle, start: t };
916                value.set(rules.dragged(grab, t.to_world(middle), &map, Modifiers::default()));
917            }),
918        );
919        let (value, space, commit) = (self.value.clone(), self.frame.space.clone(), self.on_commit.clone());
920        cx.on_key(
921            surface,
922            Rc::new(move |key, modifiers| {
923                if *key == Key::Escape
924                    && let Some(grab) = memory.grab.take()
925                {
926                    value.set(grab.start);
927                    live.set_if_changed(false);
928                    return true;
929                }
930                let map = space.map(memory.frame.get());
931                let Some(next) = rules.on_key(value.get(), key, modifiers, &map) else { return false };
932                value.set(next);
933                if let Some(commit) = &commit {
934                    commit(next);
935                }
936                true
937            }),
938        );
939        root
940    }
941}
942
943// ---------------------------------------------------------------------------------------------
944// Protractor
945// ---------------------------------------------------------------------------------------------
946
947/// An angle as people read it: `+3.5°`, `-0.1°`, `0.0°`, never `-0.0°`.
948fn angle_text(angle: f32, decimal: char) -> String {
949    let shown = (angle.abs() * 10.0).round() / 10.0;
950    let sign = if shown == 0.0 {
951        ""
952    } else if angle > 0.0 {
953        "+"
954    } else {
955        "-"
956    };
957    format!("{sign}{shown:.1}°").replace('.', &decimal.to_string())
958}
959
960/// How far the middle of the arc rises above its ends, whatever the width: the band has one height.
961const PROTRACTOR_RISE: f32 = 31.0;
962const PROTRACTOR_MARGIN: f32 = 14.0;
963const PROTRACTOR_TOP: f32 = 9.0;
964
965/// Where an angle falls on the arc drawn in `frame`.
966#[derive(Clone, Copy, Debug)]
967struct ProtractorArc {
968    frame: Rect,
969    least: f32,
970    most: f32,
971}
972
973#[cfg_attr(feature = "live", ravenrs_live::expose)]
974impl ProtractorArc {
975    /// The radius of the circle through the ends and the top of the arc.
976    fn radius(&self) -> f32 {
977        let half = (self.frame.width() / 2.0 - PROTRACTOR_MARGIN).max(1.0);
978        (half * half + PROTRACTOR_RISE * PROTRACTOR_RISE) / (2.0 * PROTRACTOR_RISE)
979    }
980
981    fn point(&self, angle: f32, inset: f32) -> Point {
982        let middle = (self.least + self.most) / 2.0;
983        let half = ((self.most - self.least) / 2.0).max(f32::EPSILON);
984        let r = self.radius();
985        let sweep = ((self.frame.width() / 2.0 - PROTRACTOR_MARGIN).max(1.0) / r).clamp(-1.0, 1.0).asin();
986        let (sin, cos) = ((angle - middle) / half * sweep).sin_cos();
987        let top = self.frame.y() + PROTRACTOR_TOP;
988        Point::new(self.frame.center().x + (r - inset) * sin, top + r - (r - inset) * cos)
989    }
990
991    fn pixels_per_degree(&self) -> f32 {
992        (self.frame.width() - PROTRACTOR_MARGIN * 2.0).max(1.0) / (self.most - self.least).max(f32::EPSILON)
993    }
994}
995
996/// A band of ticks along an arc that sets an angle, by a drag from anywhere on it. A double
997/// click goes back to zero. The arrows change it by a step (with Shift, ten), Home and End go to
998/// the ends, and Delete goes back to zero.
999///
1000/// ```
1001/// use ravenrs_reactive::State;
1002/// use ravenrs_widgets::Protractor;
1003///
1004/// let angle = State::new(0.0_f32);
1005/// let straighten = Protractor::new(angle.binding()).range(-45.0, 45.0).label("Straighten");
1006/// ```
1007#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
1008pub struct Protractor {
1009    value: Binding<f32>,
1010    least: f32,
1011    most: f32,
1012    step: f32,
1013    width: f32,
1014    on_commit: Option<Rc<dyn Fn(f32)>>,
1015    disabled: bool,
1016    label: Option<String>,
1017}
1018
1019impl fmt::Debug for Protractor {
1020    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1021        f.debug_struct("Protractor")
1022            .field("least", &self.least)
1023            .field("most", &self.most)
1024            .finish_non_exhaustive()
1025    }
1026}
1027
1028#[cfg_attr(feature = "live", ravenrs_live::expose)]
1029impl Protractor {
1030    /// A band bound to `value`, in degrees, from -45 to 45 in steps of 0.1.
1031    pub fn new(value: Binding<f32>) -> Self {
1032        Self {
1033            value,
1034            least: -45.0,
1035            most: 45.0,
1036            step: 0.1,
1037            width: 260.0,
1038            on_commit: None,
1039            disabled: false,
1040            label: None,
1041        }
1042    }
1043
1044    /// The least and the most it reaches, in degrees.
1045    pub fn range(mut self, least: f32, most: f32) -> Self {
1046        self.least = least;
1047        self.most = most.max(least + f32::EPSILON);
1048        self
1049    }
1050
1051    /// The smallest change, in degrees (0.1 by default).
1052    pub fn step(mut self, step: f32) -> Self {
1053        self.step = step.max(0.001);
1054        self
1055    }
1056
1057    /// Its width in logical pixels (260 by default).
1058    pub fn width(mut self, width: f32) -> Self {
1059        self.width = width.max(80.0);
1060        self
1061    }
1062
1063    /// Runs `handler` with the angle when a drag ends or a key or double click changes it: one
1064    /// step to undo.
1065    pub fn on_commit(mut self, handler: impl Fn(f32) + 'static) -> Self {
1066        self.on_commit = Some(Rc::new(handler));
1067        self
1068    }
1069
1070    /// Shows the angle without letting it change.
1071    pub fn disabled(mut self, disabled: bool) -> Self {
1072        self.disabled = disabled;
1073        self
1074    }
1075
1076    /// The name screen readers announce, instead of the one in [`Labels`](crate::Labels).
1077    pub fn label(mut self, label: impl Into<String>) -> Self {
1078        self.label = Some(label.into());
1079        self
1080    }
1081}
1082
1083/// On the step and in the range, and never a negative zero.
1084fn on_step(angle: f32, least: f32, most: f32, step: f32) -> f32 {
1085    let stepped = ((angle / step).round() * step * 1000.0).round() / 1000.0;
1086    let held = stepped.clamp(least, most);
1087    if held == 0.0 { 0.0 } else { held }
1088}
1089
1090impl View for Protractor {
1091    fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
1092        let theme = *cx.theme();
1093        let labels = cx.labels();
1094        let (least, most, step) = (self.least, self.most, self.step);
1095        let current = self.value.get();
1096        let grab = cx.use_state(|| Rc::new(Cell::new(None::<(f32, f32)>))).get();
1097        let disabled = self.disabled;
1098        let (band, border, major, minor, accent, ink) = (
1099            theme.color(Semantic::Control),
1100            theme.color(Semantic::Border),
1101            theme.color(if disabled { Semantic::InkMuted } else { Semantic::Ink }),
1102            theme.color(Semantic::InkMuted),
1103            theme.color(if disabled { Semantic::InkMuted } else { Semantic::Accent }),
1104            theme.color(if disabled { Semantic::InkMuted } else { Semantic::Ink }),
1105        );
1106        let radius = theme.radii.md;
1107        let span = most - least;
1108        // Few enough ticks to tell apart, on multiples people recognize.
1109        let tick = [0.5_f32, 1.0, 2.5, 5.0, 10.0, 15.0, 30.0]
1110            .into_iter()
1111            .find(|t| span / *t <= 40.0)
1112            .unwrap_or(45.0);
1113        let paint = Paint::Custom(Rc::new(move |frame: Rect, scene: &mut Scene| {
1114            scene.push_quad(Quad::new(frame, band).radius(radius).border(1.0, border));
1115            let arc = ProtractorArc { frame, least, most };
1116            let (mut small, mut large) = (Vec::new(), Vec::new());
1117            let mut at = (least / tick).ceil() * tick;
1118            while at <= most + 0.001 {
1119                let big = (at / (tick * 6.0)).fract().abs() < 0.001 || at == 0.0;
1120                let line = [arc.point(at, 0.0), arc.point(at, if big { 8.0 } else { 4.0 })];
1121                if big {
1122                    large.extend(stroke_outline(&line, 1.5));
1123                } else {
1124                    small.extend(stroke_outline(&line, 1.0));
1125                }
1126                at += tick;
1127            }
1128            scene.push_path(PathFill::solid(small, minor.with_alpha(0.7)));
1129            scene.push_path(PathFill::solid(large, major));
1130            let line = [arc.point(current, -2.0), arc.point(current, 13.0)];
1131            scene.push_path(PathFill::solid(stroke_outline(&line, 2.5), accent));
1132        }));
1133        let text = angle_text(current, labels.decimal_separator);
1134        let number = util::label(cx, &text, TextStyle::Caption, None, ink);
1135        let node = cx.container(
1136            LayoutStyle {
1137                width: Some(self.width),
1138                height: Some(52.0),
1139                padding: Insets { bottom: 5.0, ..Insets::ZERO },
1140                align: Align::Center,
1141                justify: Justify::End,
1142                align_self: Some(Align::Start),
1143                ..LayoutStyle::stack(Axis::Vertical)
1144            },
1145            &[number],
1146            paint,
1147        );
1148        let name = self.label.clone().unwrap_or_else(|| labels.protractor.clone());
1149        cx.semantics(
1150            node,
1151            Semantics::new(Role::Slider)
1152                .name(name)
1153                .value(text)
1154                .numeric(f64::from(current), f64::from(least), f64::from(most), Some(f64::from(step)))
1155                .disabled(disabled),
1156        );
1157        if disabled {
1158            return node;
1159        }
1160        cx.focusable(node, radius);
1161        cx.cursor(node, CursorIcon::ColResize);
1162        let (value, commit) = (self.value.clone(), self.on_commit.clone());
1163        let change = Rc::new(move |angle: f32, done: bool| {
1164            let angle = on_step(angle, least, most, step);
1165            if angle != value.get() {
1166                value.set(angle);
1167            }
1168            if done && let Some(commit) = &commit {
1169                commit(value.get());
1170            }
1171        });
1172        let (dragged, value) = (change.clone(), self.value.clone());
1173        cx.on_drag(
1174            node,
1175            Rc::new(move |d| {
1176                let arc = ProtractorArc { frame: d.frame, least, most };
1177                match d.phase {
1178                    DragPhase::Start => grab.set(Some((d.position.x, value.get()))),
1179                    DragPhase::Move => {
1180                        if let Some((from, angle)) = grab.get() {
1181                            dragged(angle + (d.position.x - from) / arc.pixels_per_degree(), false);
1182                        }
1183                    }
1184                    DragPhase::End => {
1185                        if let Some((from, angle)) = grab.take()
1186                            && (d.position.x - from).abs() > 0.0
1187                        {
1188                            dragged(angle + (d.position.x - from) / arc.pixels_per_degree(), true);
1189                        }
1190                    }
1191                }
1192            }),
1193        );
1194        let reset = change.clone();
1195        cx.on_double_click(node, Rc::new(move || reset(0.0, true)));
1196        let value = self.value.clone();
1197        cx.on_key(
1198            node,
1199            Rc::new(move |key, modifiers| {
1200                let by = if modifiers.shift { step * 10.0 } else { step };
1201                let angle = value.get();
1202                let next = match key {
1203                    Key::ArrowLeft | Key::ArrowDown => angle - by,
1204                    Key::ArrowRight | Key::ArrowUp => angle + by,
1205                    Key::Home => least,
1206                    Key::End => most,
1207                    Key::Delete | Key::Backspace => 0.0,
1208                    _ => return false,
1209                };
1210                change(next, true);
1211                true
1212            }),
1213        );
1214        node
1215    }
1216}
1217
1218// ---------------------------------------------------------------------------------------------
1219// CropOverlay
1220// ---------------------------------------------------------------------------------------------
1221
1222/// What a [`CropOverlay`] edits: a rectangle of the content and how far it is turned.
1223///
1224/// The picture stays still and the frame turns over it, since the renderer cannot turn a
1225/// picture. A frame turned clockwise by `angle` holds what an upright frame would hold with the
1226/// picture turned counterclockwise by it, around the middle of the rectangle.
1227#[derive(Clone, Copy, Debug, PartialEq)]
1228#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
1229pub struct CropArea {
1230    /// The rectangle before turning, in the units of the content.
1231    pub rect: Rect,
1232    /// How far it is turned clockwise, in degrees.
1233    pub angle: f32,
1234}
1235
1236#[cfg_attr(feature = "live", ravenrs_live::expose)]
1237impl CropArea {
1238    /// An upright rectangle.
1239    pub const fn new(rect: Rect) -> Self {
1240        Self { rect, angle: 0.0 }
1241    }
1242
1243    /// The same rectangle turned clockwise by this many degrees.
1244    pub const fn rotated(mut self, angle: f32) -> Self {
1245        self.angle = angle;
1246        self
1247    }
1248
1249    /// Its four corners once turned, clockwise from what was the top left, in the units of the
1250    /// content.
1251    pub fn corners(&self) -> [Point; 4] {
1252        BoxTransform::new(self.rect).rotated(self.angle).corners()
1253    }
1254}
1255
1256/// A crop over a picture: what is out of the frame goes dark, with thirds inside the frame and
1257/// a fine grid while it turns. It is built on [`TransformHandles`] and draws the same lines as
1258/// [`RuleOfThirdsGrid`](crate::RuleOfThirdsGrid).
1259///
1260/// Handles resize it, a drag inside moves it, a drag outside turns it, and it never leaves the
1261/// content: turning makes it smaller if it must. It hands back the rectangle and the angle and
1262/// does not turn the picture, since the renderer has no transforms.
1263///
1264/// Keyboard: the arrows move it, with Alt they resize it, `[` and `]` turn it, and Shift makes
1265/// each of these go further. A [`Protractor`] can share the angle through
1266/// [`Binding::map`](ravenrs_reactive::Binding::map).
1267///
1268/// ```
1269/// use ravenrs_core::{Rect, Size};
1270/// use ravenrs_reactive::State;
1271/// use ravenrs_widgets::{ContentSpace, CropArea, CropOverlay, Protractor};
1272///
1273/// let crop = State::new(CropArea::new(Rect::new(300.0, 200.0, 1800.0, 1200.0)));
1274/// let overlay = CropOverlay::new(crop.binding())
1275///     .aspect(Some(3.0 / 2.0))
1276///     .space(ContentSpace::Fit(Size::new(2400.0, 1600.0)))
1277///     .size(600.0, 400.0);
1278/// let straighten = Protractor::new(crop.binding().map(|c| c.angle, |c, angle| c.angle = angle));
1279/// ```
1280#[derive(Debug)]
1281#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
1282pub struct CropOverlay {
1283    handles: TransformHandles,
1284}
1285
1286#[cfg_attr(feature = "live", ravenrs_live::expose)]
1287impl CropOverlay {
1288    /// A crop bound to `area`, in the units of its [space](Self::space).
1289    pub fn new(area: Binding<CropArea>) -> Self {
1290        let value = area.map(
1291            |a| BoxTransform::new(a.rect).rotated(a.angle),
1292            |a, t: BoxTransform| {
1293                a.rect = t.rect;
1294                a.angle = t.angle;
1295            },
1296        );
1297        let mut handles = TransformHandles::new(value);
1298        handles.rules = Rules {
1299            keep_corners: false,
1300            aspect: None,
1301            confined: true,
1302            min_size: 24.0,
1303            knob: false,
1304            pivot: false,
1305            outside_rotates: true,
1306            angle_limit: Some(45.0),
1307            shrink_to_turn: true,
1308        };
1309        handles.look = Look {
1310            veil: true,
1311            guide: Some(CompositionGuide::Thirds),
1312            turning_guide: Some(CompositionGuide::Divisions(8)),
1313        };
1314        handles.crop_name = true;
1315        Self { handles }
1316    }
1317
1318    /// Holds this width over height, or lets it go free with `None`. A crop of other proportions
1319    /// is cut down to these as soon as this changes.
1320    pub fn aspect(mut self, aspect: Option<f32>) -> Self {
1321        self.handles.rules.aspect = aspect;
1322        self
1323    }
1324
1325    /// How far it can turn either way, in degrees (45 by default). With 0 it does not turn.
1326    pub fn angle_limit(mut self, degrees: f32) -> Self {
1327        let degrees = degrees.abs();
1328        self.handles.rules.angle_limit = Some(degrees);
1329        self.handles.rules.outside_rotates = degrees > 0.0;
1330        self
1331    }
1332
1333    /// The lines inside the frame, or none (thirds by default).
1334    pub fn guide(mut self, guide: Option<CompositionGuide>) -> Self {
1335        self.handles.look.guide = guide;
1336        self
1337    }
1338
1339    /// The lines inside the frame while it turns (eight parts by default).
1340    pub fn turning_guide(mut self, guide: Option<CompositionGuide>) -> Self {
1341        self.handles.look.turning_guide = guide;
1342        self
1343    }
1344
1345    /// The smallest side, in logical pixels on screen at the current zoom (24 by default).
1346    pub fn min_size(mut self, pixels: f32) -> Self {
1347        self.handles.rules.min_size = pixels.max(1.0);
1348        self
1349    }
1350
1351    /// Runs `handler` with the result when a drag ends or a key changes it: one step to undo.
1352    pub fn on_commit(mut self, handler: impl Fn(CropArea) + 'static) -> Self {
1353        self.handles.on_commit = Some(Rc::new(move |t: BoxTransform| {
1354            handler(CropArea { rect: t.rect, angle: t.angle });
1355        }));
1356        self
1357    }
1358
1359    /// Takes no input while disabled.
1360    pub fn disabled(mut self, disabled: bool) -> Self {
1361        self.handles.disabled = disabled;
1362        self
1363    }
1364
1365    /// The name screen readers announce, instead of the one in [`Labels`](crate::Labels).
1366    pub fn label(mut self, label: impl Into<String>) -> Self {
1367        self.handles.label = Some(label.into());
1368        self
1369    }
1370}
1371
1372overlay_builders!(CropOverlay, handles.frame);
1373
1374impl View for CropOverlay {
1375    fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
1376        self.handles.build(cx)
1377    }
1378}