1use 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#[derive(Clone, Copy, Debug, PartialEq)]
33#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
34pub struct BoxTransform {
35 pub rect: Rect,
37 pub angle: f32,
39 pub pivot: Point,
41}
42
43#[cfg_attr(feature = "live", ravenrs_live::expose)]
44impl BoxTransform {
45 pub const fn new(rect: Rect) -> Self {
47 Self { rect, angle: 0.0, pivot: Point::new(0.5, 0.5) }
48 }
49
50 pub const fn rotated(mut self, angle: f32) -> Self {
52 self.angle = angle;
53 self
54 }
55
56 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 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 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 pub fn corners(&self) -> [Point; 4] {
83 corners_of(self.rect).map(|p| self.to_world(p))
84 }
85
86 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 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 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#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
151#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
152pub enum BoxHandle {
153 TopLeft,
155 Top,
157 TopRight,
159 Right,
161 BottomRight,
163 Bottom,
165 BottomLeft,
167 Left,
169}
170
171#[cfg_attr(feature = "live", ravenrs_live::expose)]
172impl BoxHandle {
173 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 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#[derive(Clone, Copy, Debug)]
206struct Resize {
207 handle: BoxHandle,
208 ratio: Option<f32>,
210 around_pivot: bool,
211 least: Size,
212}
213
214#[cfg_attr(feature = "live", ravenrs_live::expose)]
215impl Resize {
216 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 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 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 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 self.apply(start, across, down).moved_inside(bounds)
288 }
289}
290
291fn 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
297const HANDLE_REACH: f32 = 11.0;
304const EDGE_REACH: f32 = 5.0;
306const HANDLE_SIDE: f32 = 8.0;
307const 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 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
355fn 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 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
406fn 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 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 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 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 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 Some(self.resized(handle, t, dx, dy, map, Modifiers::default()))
564 }
565}
566
567#[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 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 pub fn keep_proportions(mut self, keep: bool) -> Self {
637 self.rules.keep_corners = keep;
638 self
639 }
640
641 pub fn aspect(mut self, aspect: Option<f32>) -> Self {
643 self.rules.aspect = aspect;
644 self
645 }
646
647 pub fn confined(mut self, confined: bool) -> Self {
649 self.rules.confined = confined;
650 self
651 }
652
653 pub fn min_size(mut self, pixels: f32) -> Self {
655 self.rules.min_size = pixels.max(1.0);
656 self
657 }
658
659 pub fn rotatable(mut self, rotatable: bool) -> Self {
661 self.rules.knob = rotatable;
662 self
663 }
664
665 pub fn rotate_outside(mut self, rotate: bool) -> Self {
667 self.rules.outside_rotates = rotate;
668 self
669 }
670
671 pub fn pivot_handle(mut self, show: bool) -> Self {
673 self.rules.pivot = show;
674 self
675 }
676
677 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 pub fn disabled(mut self, disabled: bool) -> Self {
685 self.disabled = disabled;
686 self
687 }
688
689 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#[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 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 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
943fn 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
960const PROTRACTOR_RISE: f32 = 31.0;
962const PROTRACTOR_MARGIN: f32 = 14.0;
963const PROTRACTOR_TOP: f32 = 9.0;
964
965#[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 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#[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 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 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 pub fn step(mut self, step: f32) -> Self {
1053 self.step = step.max(0.001);
1054 self
1055 }
1056
1057 pub fn width(mut self, width: f32) -> Self {
1059 self.width = width.max(80.0);
1060 self
1061 }
1062
1063 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 pub fn disabled(mut self, disabled: bool) -> Self {
1072 self.disabled = disabled;
1073 self
1074 }
1075
1076 pub fn label(mut self, label: impl Into<String>) -> Self {
1078 self.label = Some(label.into());
1079 self
1080 }
1081}
1082
1083fn 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 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#[derive(Clone, Copy, Debug, PartialEq)]
1228#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
1229pub struct CropArea {
1230 pub rect: Rect,
1232 pub angle: f32,
1234}
1235
1236#[cfg_attr(feature = "live", ravenrs_live::expose)]
1237impl CropArea {
1238 pub const fn new(rect: Rect) -> Self {
1240 Self { rect, angle: 0.0 }
1241 }
1242
1243 pub const fn rotated(mut self, angle: f32) -> Self {
1245 self.angle = angle;
1246 self
1247 }
1248
1249 pub fn corners(&self) -> [Point; 4] {
1252 BoxTransform::new(self.rect).rotated(self.angle).corners()
1253 }
1254}
1255
1256#[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 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 pub fn aspect(mut self, aspect: Option<f32>) -> Self {
1321 self.handles.rules.aspect = aspect;
1322 self
1323 }
1324
1325 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 pub fn guide(mut self, guide: Option<CompositionGuide>) -> Self {
1335 self.handles.look.guide = guide;
1336 self
1337 }
1338
1339 pub fn turning_guide(mut self, guide: Option<CompositionGuide>) -> Self {
1341 self.handles.look.turning_guide = guide;
1342 self
1343 }
1344
1345 pub fn min_size(mut self, pixels: f32) -> Self {
1347 self.handles.rules.min_size = pixels.max(1.0);
1348 self
1349 }
1350
1351 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 pub fn disabled(mut self, disabled: bool) -> Self {
1361 self.handles.disabled = disabled;
1362 self
1363 }
1364
1365 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}