1use std::cell::Cell;
5use std::fmt;
6use std::rc::Rc;
7
8use ravenrs_core::{
9 Color, CursorIcon, Key, Modifiers, PathElement, PathFill, Point, Quad, Rect, Role, Scene, Size,
10};
11use ravenrs_layout::{Align, Axis, LayoutStyle};
12use ravenrs_reactive::{Binding, Observer, State};
13use ravenrs_style::{Semantic, Theme};
14
15use super::{ZoomView, stroke_outline};
16use crate::context::{BuildCx, DragPhase, NodeId, Paint, Semantics};
17use crate::{AnyView, ButtonSize, IconButton, IconName, Tooltip, View};
18
19#[derive(Clone, Debug, Default)]
26#[non_exhaustive]
27#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
28pub enum ContentSpace {
29 #[default]
31 Frame,
32 Fit(Size),
34 View(Binding<ZoomView>, Size),
37}
38
39#[cfg_attr(feature = "live", ravenrs_live::expose)]
40impl ContentSpace {
41 pub fn placed(&self, frame: Rect) -> Rect {
43 match self {
44 Self::Frame => frame,
45 Self::Fit(content) => fitted(frame, *content),
46 Self::View(view, content) => {
47 let mut v = view.get();
48 v.viewport = frame.size;
49 if v.fit {
50 v.scale = v.fit_scale(*content);
51 v.center = Point::new(0.5, 0.5);
52 }
53 let placed = v.placed(*content);
54 placed.translate(frame.origin)
55 }
56 }
57 }
58
59 pub fn content_size(&self, frame: Rect) -> Size {
61 match self {
62 Self::Frame => frame.size,
63 Self::Fit(content) | Self::View(_, content) => *content,
64 }
65 }
66
67 pub fn scale(&self, frame: Rect) -> f32 {
69 self.map(frame).scale
70 }
71
72 pub fn to_content(&self, frame: Rect, at: Point) -> Point {
74 self.map(frame).content(at)
75 }
76
77 pub fn to_screen(&self, frame: Rect, at: Point) -> Point {
79 self.map(frame).screen(at)
80 }
81
82 pub fn rect_to_screen(&self, frame: Rect, rect: Rect) -> Rect {
84 self.map(frame).screen_rect(rect)
85 }
86
87 pub(super) fn map(&self, frame: Rect) -> ContentMap {
88 let placed = self.placed(frame);
89 let content = self.content_size(frame);
90 ContentMap { placed, scale: placed.width() / content.width.max(f32::EPSILON), content }
91 }
92
93 pub(super) fn watch(&self) {
95 if let Self::View(view, _) = self {
96 view.get();
97 }
98 }
99}
100
101pub(super) fn fitted(frame: Rect, content: Size) -> Rect {
103 let scale = (frame.width() / content.width.max(f32::EPSILON))
104 .min(frame.height() / content.height.max(f32::EPSILON))
105 .max(0.0);
106 let (w, h) = (content.width * scale, content.height * scale);
107 Rect::new(frame.x() + (frame.width() - w) / 2.0, frame.y() + (frame.height() - h) / 2.0, w, h)
108}
109
110#[derive(Clone, Copy, Debug)]
112pub(super) struct ContentMap {
113 pub placed: Rect,
114 pub scale: f32,
115 pub content: Size,
116}
117
118#[cfg_attr(feature = "live", ravenrs_live::expose)]
119impl ContentMap {
120 pub fn screen(&self, at: Point) -> Point {
121 Point::new(self.placed.x() + at.x * self.scale, self.placed.y() + at.y * self.scale)
122 }
123
124 pub fn content(&self, at: Point) -> Point {
125 let scale = self.scale.max(f32::EPSILON);
126 Point::new((at.x - self.placed.x()) / scale, (at.y - self.placed.y()) / scale)
127 }
128
129 pub fn screen_rect(&self, rect: Rect) -> Rect {
130 let at = self.screen(rect.origin);
131 Rect::new(at.x, at.y, rect.width() * self.scale, rect.height() * self.scale)
132 }
133
134 pub fn bounds(&self) -> Rect {
135 Rect::new(0.0, 0.0, self.content.width, self.content.height)
136 }
137}
138
139fn spanned(a: Point, b: Point) -> Rect {
141 Rect::new(a.x.min(b.x), a.y.min(b.y), (b.x - a.x).abs(), (b.y - a.y).abs())
142}
143
144pub(super) fn picture_inks(theme: &Theme) -> (Color, Color) {
147 let brightness = |c: Color| 0.2126 * c.r + 0.7152 * c.g + 0.0722 * c.b;
148 let (a, b) = (theme.color(Semantic::Canvas), theme.color(Semantic::Ink));
149 if brightness(a) > brightness(b) { (a, b) } else { (b, a) }
150}
151
152#[derive(Default)]
159pub(super) struct OverlayBox {
160 width: Option<f32>,
161 height: Option<f32>,
162 grow: Option<f32>,
163 stage: Option<AnyView>,
164 pub space: ContentSpace,
165}
166
167impl fmt::Debug for OverlayBox {
168 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169 f.debug_struct("OverlayBox").field("space", &self.space).finish_non_exhaustive()
170 }
171}
172
173#[cfg_attr(feature = "live", ravenrs_live::expose)]
174impl OverlayBox {
175 pub fn size(&mut self, width: f32, height: f32) {
176 self.width = Some(width);
177 self.height = Some(height);
178 self.grow = Some(0.0);
179 }
180
181 pub fn grow(&mut self, grow: f32) {
182 self.grow = Some(grow);
183 }
184
185 pub fn stage(&mut self, stage: AnyView) {
186 self.stage = Some(stage);
187 }
188
189 pub fn known_size(&self, cx: &BuildCx<'_>) -> Option<Size> {
191 match (self.width, self.height) {
192 (Some(width), Some(height)) => Some(Size::new(width, height)),
193 _ => cx.scroll_viewport(),
194 }
195 }
196
197 pub fn build(&self, cx: &mut BuildCx<'_>, paint: Paint) -> (NodeId, NodeId) {
199 let sized = self.width.is_some();
200 let Some(stage) = &self.stage else {
201 let grow = self.grow.unwrap_or(1.0);
202 let root = cx.container(
203 LayoutStyle {
204 width: self.width,
205 height: self.height,
206 grow,
207 align_self: Some(if sized { Align::Start } else { Align::Stretch }),
208 ..LayoutStyle::leaf()
209 },
210 &[],
211 paint,
212 );
213 cx.report_frame(root);
214 return (root, root);
215 };
216 let stage = cx.child(stage.as_ref());
217 let measured = cx.scroll_viewport().unwrap_or(Size::ZERO);
220 let surface = cx.container(
221 LayoutStyle {
222 absolute: Some((0.0, 0.0)),
223 width: Some(measured.width),
224 height: Some(measured.height),
225 ..LayoutStyle::leaf()
226 },
227 &[],
228 paint,
229 );
230 let grow = self.grow.unwrap_or(0.0);
231 let root = cx.container(
232 LayoutStyle {
233 width: self.width,
234 height: self.height,
235 grow,
236 align: Align::Stretch,
237 align_self: Some(if grow > 0.0 { Align::Stretch } else { Align::Start }),
238 ..LayoutStyle::stack(Axis::Vertical)
239 },
240 &[stage, surface],
241 Paint::None,
242 );
243 cx.report_frame(root);
244 (root, surface)
245 }
246}
247
248macro_rules! overlay_builders {
250 ($ty:ident, $($field:ident).+) => {
251 #[cfg_attr(feature = "live", ravenrs_live::expose(part = "builders"))]
252 impl $ty {
253 pub fn size(mut self, width: f32, height: f32) -> Self {
255 self.$($field).+.size(width, height);
256 self
257 }
258
259 pub fn grow(mut self, grow: f32) -> Self {
262 self.$($field).+.grow(grow);
263 self
264 }
265
266 pub fn stage(mut self, stage: impl View + 'static) -> Self {
270 self.$($field).+.stage(Box::new(stage));
271 self
272 }
273
274 pub fn space(mut self, space: ContentSpace) -> Self {
276 self.$($field).+.space = space;
277 self
278 }
279 }
280 };
281}
282pub(super) use overlay_builders;
283
284pub(super) fn gesture_paint(live: bool, draw: impl Fn(Rect, &mut Scene) + 'static) -> Paint {
287 if live {
288 Paint::Live(Rc::new(move |frame, _, scene| draw(frame, scene)))
289 } else {
290 Paint::Custom(Rc::new(draw))
291 }
292}
293
294#[derive(Clone, Copy, Debug, Default, PartialEq)]
300#[non_exhaustive]
301#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
302pub enum CompositionGuide {
303 #[default]
305 Thirds,
306 Divisions(u32),
308 Squares(u32),
310 GoldenRatio,
312}
313
314#[cfg_attr(feature = "live", ravenrs_live::expose)]
315impl CompositionGuide {
316 fn lines(self, width: f32, height: f32) -> (Vec<f32>, Vec<f32>) {
318 let equal = |parts: u32| -> Vec<f32> { (1..parts).map(|i| i as f32 / parts as f32).collect() };
319 match self {
320 Self::Thirds => (equal(3), equal(3)),
321 Self::Divisions(parts) => (equal(parts), equal(parts)),
322 Self::GoldenRatio => {
323 let golden = 1.0 / 1.618_034;
324 (vec![1.0 - golden, golden], vec![1.0 - golden, golden])
325 }
326 Self::Squares(cells) => {
327 let side = width.min(height) / cells.max(1) as f32;
328 let along = |length: f32| -> Vec<f32> {
329 if side <= 0.0 {
330 return Vec::new();
331 }
332 (1..)
333 .map(|i| i as f32 * side)
334 .take_while(|at| *at < length - 0.5)
335 .map(|at| at / length)
336 .collect()
337 };
338 (along(width), along(height))
339 }
340 }
341 }
342}
343
344pub(super) fn paint_guide(
346 scene: &mut Scene,
347 corners: [Point; 4],
348 guide: CompositionGuide,
349 light: Color,
350 dark: Color,
351 opacity: f32,
352) {
353 let [tl, tr, br, bl] = corners;
354 let (width, height) = ((tr.x - tl.x).hypot(tr.y - tl.y), (bl.x - tl.x).hypot(bl.y - tl.y));
355 if width <= 0.0 || height <= 0.0 {
356 return;
357 }
358 let (across, down) = guide.lines(width, height);
359 let mix = |a: Point, b: Point, t: f32| Point::new(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t);
360 let (light, dark) = (light.with_alpha(light.a * opacity), dark.with_alpha(dark.a * opacity * 0.5));
361 let lines = across
362 .iter()
363 .map(|t| (mix(tl, tr, *t), mix(bl, br, *t)))
364 .chain(down.iter().map(|t| (mix(tl, bl, *t), mix(tr, br, *t))));
365 if (tl.y - tr.y).abs() < 0.01 {
366 for (a, b) in lines {
369 let (x, y) = (a.x.round(), a.y.round());
370 let (w, h) = ((b.x - a.x).abs().max(1.0), (b.y - a.y).abs().max(1.0));
371 let (dx, dy) = if w <= 1.0 { (1.0, 0.0) } else { (0.0, 1.0) };
372 scene.push_quad(Quad::new(Rect::new(x + dx, y + dy, w, h), dark));
373 scene.push_quad(Quad::new(Rect::new(x, y, w, h), light));
374 }
375 } else {
376 let mut outline = Vec::new();
377 for (a, b) in lines {
378 outline.extend(stroke_outline(&[a, b], 1.0));
379 }
380 if !outline.is_empty() {
381 scene.push_path(PathFill::solid(outline, light));
382 }
383 }
384}
385
386pub(super) fn corners_of(rect: Rect) -> [Point; 4] {
388 [
389 rect.origin,
390 Point::new(rect.max_x(), rect.y()),
391 Point::new(rect.max_x(), rect.max_y()),
392 Point::new(rect.x(), rect.max_y()),
393 ]
394}
395
396#[derive(Debug, Default)]
409#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
410pub struct RuleOfThirdsGrid {
411 frame: OverlayBox,
412 guide: CompositionGuide,
413 area: Option<Rect>,
414 color: Option<Color>,
415}
416
417#[cfg_attr(feature = "live", ravenrs_live::expose)]
418impl RuleOfThirdsGrid {
419 pub fn new() -> Self {
421 Self::default()
422 }
423
424 pub fn guide(mut self, guide: CompositionGuide) -> Self {
426 self.guide = guide;
427 self
428 }
429
430 pub fn area(mut self, area: Rect) -> Self {
432 self.area = Some(area);
433 self
434 }
435
436 pub fn color(mut self, color: Color) -> Self {
438 self.color = Some(color);
439 self
440 }
441}
442
443overlay_builders!(RuleOfThirdsGrid, frame);
444
445impl View for RuleOfThirdsGrid {
446 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
447 let (mut light, mut dark) = picture_inks(cx.theme());
448 if let Some(color) = self.color {
449 light = color;
450 dark = Color::TRANSPARENT;
451 }
452 self.frame.space.watch();
453 let (space, area, guide) = (self.frame.space.clone(), self.area, self.guide);
454 let paint = Paint::Custom(Rc::new(move |frame: Rect, scene: &mut Scene| {
455 let map = space.map(frame);
456 let shown = area.map_or(map.placed, |area| map.screen_rect(area));
457 scene.push_clip(frame);
458 paint_guide(scene, corners_of(shown), guide, light, dark, 0.45);
459 scene.pop_clip();
460 }));
461 let (root, surface) = self.frame.build(cx, paint);
462 cx.semantics(surface, Semantics::hidden());
464 root
465 }
466}
467
468const BAND_MIN_DRAG: f32 = 4.0;
474
475#[derive(Debug, Default)]
476struct BandMemory {
477 frame: Cell<Rect>,
478 pressed: Cell<Option<Point>>,
480}
481
482#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
505pub struct RubberBand {
506 selection: Binding<Option<Rect>>,
507 frame: OverlayBox,
508 on_select: Option<Rc<dyn Fn(Rect)>>,
509 keep: bool,
510 disabled: bool,
511 label: Option<String>,
512}
513
514impl fmt::Debug for RubberBand {
515 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
516 f.debug_struct("RubberBand").field("frame", &self.frame).finish_non_exhaustive()
517 }
518}
519
520#[cfg_attr(feature = "live", ravenrs_live::expose)]
521impl RubberBand {
522 pub fn new(selection: Binding<Option<Rect>>) -> Self {
524 Self {
525 selection,
526 frame: OverlayBox::default(),
527 on_select: None,
528 keep: true,
529 disabled: false,
530 label: None,
531 }
532 }
533
534 pub fn on_select(mut self, handler: impl Fn(Rect) + 'static) -> Self {
536 self.on_select = Some(Rc::new(handler));
537 self
538 }
539
540 pub fn keep(mut self, keep: bool) -> Self {
543 self.keep = keep;
544 self
545 }
546
547 pub fn disabled(mut self, disabled: bool) -> Self {
550 self.disabled = disabled;
551 self
552 }
553
554 pub fn label(mut self, label: impl Into<String>) -> Self {
556 self.label = Some(label.into());
557 self
558 }
559}
560
561overlay_builders!(RubberBand, frame);
562
563fn kept_inside(rect: Rect, bounds: Rect) -> Rect {
565 let (w, h) = (rect.width().min(bounds.width()), rect.height().min(bounds.height()));
566 Rect::new(
567 rect.x().clamp(bounds.x(), (bounds.max_x() - w).max(bounds.x())),
568 rect.y().clamp(bounds.y(), (bounds.max_y() - h).max(bounds.y())),
569 w,
570 h,
571 )
572}
573
574fn clamped(at: Point, bounds: Rect) -> Point {
575 Point::new(at.x.clamp(bounds.x(), bounds.max_x()), at.y.clamp(bounds.y(), bounds.max_y()))
576}
577
578#[cfg_attr(feature = "live", ravenrs_live::expose(part = "part1"))]
579impl RubberBand {
580 fn on_key(
581 selection: &Binding<Option<Rect>>,
582 memory: &BandMemory,
583 space: &ContentSpace,
584 on_select: Option<&Rc<dyn Fn(Rect)>>,
585 key: &Key,
586 modifiers: Modifiers,
587 ) -> bool {
588 let current = selection.get();
589 match key {
590 Key::Escape if memory.pressed.get().is_some() || current.is_some() => {
591 memory.pressed.set(None);
592 selection.set(None);
593 return true;
594 }
595 Key::Delete | Key::Backspace if current.is_some() => {
596 selection.set(None);
597 return true;
598 }
599 Key::Enter => {
600 let (Some(rect), Some(handler)) = (current, on_select) else { return false };
601 handler(rect);
602 return true;
603 }
604 _ => {}
605 }
606 let (dx, dy) = match key {
607 Key::ArrowLeft => (-1.0, 0.0),
608 Key::ArrowRight => (1.0, 0.0),
609 Key::ArrowUp => (0.0, -1.0),
610 Key::ArrowDown => (0.0, 1.0),
611 _ => return false,
612 };
613 let map = space.map(memory.frame.get());
614 let bounds = map.bounds();
615 let Some(rect) = current else {
616 let (w, h) = (bounds.width() / 2.0, bounds.height() / 2.0);
617 selection.set(Some(Rect::new(w / 2.0, h / 2.0, w, h)));
618 return true;
619 };
620 let step = (if modifiers.shift { 10.0 } else { 1.0 }) / map.scale.max(f32::EPSILON);
621 let next = if modifiers.alt {
622 let least = BAND_MIN_DRAG / map.scale.max(f32::EPSILON);
623 Rect::new(
624 rect.x(),
625 rect.y(),
626 (rect.width() + dx * step).max(least),
627 (rect.height() + dy * step).max(least),
628 )
629 } else {
630 Rect::new(rect.x() + dx * step, rect.y() + dy * step, rect.width(), rect.height())
631 };
632 selection.set(Some(kept_inside(next, bounds)));
633 true
634 }
635}
636
637impl View for RubberBand {
638 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
639 let theme = *cx.theme();
640 let labels = cx.labels();
641 let live = cx.use_state(|| false);
642 let is_live = live.get();
643 let current = if is_live {
646 Observer::untracked(|| self.selection.get())
647 } else {
648 self.frame.space.watch();
649 self.selection.get()
650 };
651 let memory = cx.use_state(|| Rc::new(BandMemory::default())).get();
652 let (light, dark) = picture_inks(&theme);
653 let (space, selection, remembered) =
654 (self.frame.space.clone(), self.selection.clone(), memory.clone());
655 let paint = gesture_paint(is_live, move |frame, scene| {
656 remembered.frame.set(frame);
657 let Some(rect) = selection.get() else { return };
658 let shown = space.rect_to_screen(frame, rect);
659 scene.push_clip(frame);
660 scene.push_quad(Quad::new(shown, light.with_alpha(0.12)).border(1.0, light));
661 scene.push_quad(
662 Quad::new(shown.inflate(1.0), Color::TRANSPARENT).border(1.0, dark.with_alpha(0.55)),
663 );
664 scene.pop_clip();
665 });
666 let (root, surface) = self.frame.build(cx, paint);
667 let name = self.label.clone().unwrap_or_else(|| labels.rubber_band.clone());
668 let mut semantics = Semantics::new(Role::Group).name(name).disabled(self.disabled);
669 if let Some(rect) = current {
670 semantics = semantics.value(format!(
671 "{:.0}, {:.0}, {:.0} × {:.0}",
672 rect.x(),
673 rect.y(),
674 rect.width(),
675 rect.height()
676 ));
677 }
678 cx.semantics(surface, semantics);
679 if self.disabled {
680 return root;
681 }
682 cx.focusable(surface, 0.0);
683 cx.cursor(surface, CursorIcon::Default);
684 let (selection, space, on_select, keep, grabbed) = (
685 self.selection.clone(),
686 self.frame.space.clone(),
687 self.on_select.clone(),
688 self.keep,
689 memory.clone(),
690 );
691 cx.on_drag(
692 surface,
693 Rc::new(move |d| {
694 let map = space.map(d.frame);
695 let at = clamped(map.content(d.position), map.bounds());
696 match d.phase {
697 DragPhase::Start => {
698 grabbed.pressed.set(Some(at));
699 live.set_if_changed(true);
700 }
701 DragPhase::Move => {
702 let Some(pressed) = grabbed.pressed.get() else { return };
703 let rect = spanned(pressed, at);
704 let far = BAND_MIN_DRAG / map.scale.max(f32::EPSILON);
705 if rect.width().max(rect.height()) >= far || selection.get().is_some() {
706 selection.set(Some(rect));
707 }
708 }
709 DragPhase::End => {
710 live.set_if_changed(false);
711 let Some(pressed) = grabbed.pressed.take() else { return };
712 let rect = spanned(pressed, at);
713 let far = BAND_MIN_DRAG / map.scale.max(f32::EPSILON);
714 if rect.width().max(rect.height()) < far {
715 if selection.get().is_some() {
716 selection.set(None);
717 }
718 return;
719 }
720 if let Some(handler) = &on_select {
721 handler(rect);
722 }
723 if !keep {
724 selection.set(None);
725 }
726 }
727 }
728 }),
729 );
730 let (selection, space, on_select) =
731 (self.selection.clone(), self.frame.space.clone(), self.on_select.clone());
732 cx.on_key(
733 surface,
734 Rc::new(move |key, modifiers| {
735 Self::on_key(&selection, &memory, &space, on_select.as_ref(), key, modifiers)
736 }),
737 );
738 root
739 }
740}
741
742const MARQUEE_DASH: f32 = 4.0;
748const MARQUEE_SPEED: f32 = 16.0;
750
751struct Strokes<'a> {
753 scene: &'a mut Scene,
754 frame: Rect,
755 width: f32,
756 color: Color,
757 path: Vec<PathElement>,
758}
759
760#[cfg_attr(feature = "live", ravenrs_live::expose)]
761impl Strokes<'_> {
762 fn line(&mut self, a: Point, b: Point) {
763 let (x, y) = (a.x.min(b.x), a.y.min(b.y));
764 let (w, h) = ((b.x - a.x).abs(), (b.y - a.y).abs());
765 let bounds = if h < 0.01 {
768 Rect::new(x, y.round(), w.max(1.0), 1.0)
769 } else if w < 0.01 {
770 Rect::new(x.round(), y, 1.0, h.max(1.0))
771 } else {
772 let half = self.width / 2.0;
773 if Rect::new(x, y, w, h).inflate(half).intersect(&self.frame).is_empty() {
774 return;
775 }
776 self.path.extend(stroke_outline(&[a, b], self.width));
777 return;
778 };
779 if !bounds.intersect(&self.frame).is_empty() {
780 self.scene.push_quad(Quad::new(bounds, self.color));
781 }
782 }
783
784 fn finish(self) {
785 if !self.path.is_empty() {
786 self.scene.push_path(PathFill::solid(self.path, self.color));
787 }
788 }
789}
790
791fn march(points: &[Point], phase: f32, mut dash: impl FnMut(Point, Point)) {
794 let period = MARQUEE_DASH * 2.0;
795 let mut walked = 0.0;
796 for pair in points.windows(2) {
797 let (a, b) = (pair[0], pair[1]);
798 let length = (b.x - a.x).hypot(b.y - a.y);
799 if length <= 0.0 {
800 continue;
801 }
802 let at = |t: f32| Point::new(a.x + (b.x - a.x) * t / length, a.y + (b.y - a.y) * t / length);
803 let mut t = 0.0;
804 while t < length {
805 let place = (walked + t - phase).rem_euclid(period);
806 if place < MARQUEE_DASH {
807 let end = (t + MARQUEE_DASH - place).min(length);
808 dash(at(t), at(end));
809 t = end.max(t + 0.001);
810 } else {
811 t += (period - place).max(0.001);
812 }
813 }
814 walked += length;
815 }
816}
817
818#[derive(Debug)]
832#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
833pub struct Marquee {
834 lines: Rc<[Vec<Point>]>,
835 frame: OverlayBox,
836 animated: bool,
837 label: Option<String>,
838}
839
840#[cfg_attr(feature = "live", ravenrs_live::expose)]
841impl Marquee {
842 pub fn rect(rect: Rect) -> Self {
844 let [a, b, c, d] = corners_of(rect);
845 Self::from_lines(vec![vec![a, b, c, d, a]])
846 }
847
848 pub fn path(points: impl IntoIterator<Item = Point>) -> Self {
850 let mut points: Vec<Point> = points.into_iter().collect();
851 if let (Some(first), true) = (points.first().copied(), points.len() > 2) {
852 points.push(first);
853 }
854 Self::from_lines(vec![points])
855 }
856
857 pub fn segments(segments: impl IntoIterator<Item = (Point, Point)>) -> Self {
859 Self::from_lines(segments.into_iter().map(|(a, b)| vec![a, b]).collect())
860 }
861
862 fn from_lines(lines: Vec<Vec<Point>>) -> Self {
863 Self { lines: lines.into(), frame: OverlayBox::default(), animated: true, label: None }
864 }
865
866 pub fn animated(mut self, animated: bool) -> Self {
868 self.animated = animated;
869 self
870 }
871
872 pub fn label(mut self, label: impl Into<String>) -> Self {
874 self.label = Some(label.into());
875 self
876 }
877}
878
879overlay_builders!(Marquee, frame);
880
881impl View for Marquee {
882 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
883 let (light, dark) = picture_inks(cx.theme());
884 let name = self.label.clone().unwrap_or_else(|| cx.labels().marquee.clone());
885 self.frame.space.watch();
886 let (lines, space) = (self.lines.clone(), self.frame.space.clone());
887 let draw = move |frame: Rect, phase: f32, scene: &mut Scene| {
888 let map = space.map(frame);
889 let shown: Vec<Vec<Point>> =
890 lines.iter().map(|line| line.iter().map(|p| map.screen(*p)).collect()).collect();
891 scene.push_clip(frame);
892 let mut base = Strokes { scene, frame, width: 1.5, color: dark, path: Vec::new() };
893 for line in &shown {
894 for pair in line.windows(2) {
895 base.line(pair[0], pair[1]);
896 }
897 }
898 base.finish();
899 let mut dashes = Strokes { scene, frame, width: 1.0, color: light, path: Vec::new() };
900 for line in &shown {
901 march(line, phase, |a, b| dashes.line(a, b));
902 }
903 dashes.finish();
904 scene.pop_clip();
905 };
906 let paint = if self.animated && !cx.reduced_motion() {
907 Paint::Live(Rc::new(move |frame, time, scene| draw(frame, time as f32 * MARQUEE_SPEED, scene)))
908 } else {
909 Paint::Custom(Rc::new(move |frame, scene| draw(frame, 0.0, scene)))
910 };
911 let (root, surface) = self.frame.build(cx, paint);
912 cx.semantics(surface, Semantics::new(Role::Image).name(name));
913 root
914 }
915}
916
917#[derive(Clone)]
925#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
926pub struct EyedropperState {
927 armed: State<bool>,
928 picked: State<Option<Color>>,
929 on_pick: Option<Rc<dyn Fn(Color)>>,
930 stay_armed: bool,
931}
932
933impl fmt::Debug for EyedropperState {
934 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
935 f.debug_struct("EyedropperState").field("armed", &self.armed.get()).finish_non_exhaustive()
936 }
937}
938
939impl Default for EyedropperState {
940 fn default() -> Self {
941 Self::new()
942 }
943}
944
945#[cfg_attr(feature = "live", ravenrs_live::expose)]
946impl EyedropperState {
947 pub fn new() -> Self {
949 Self { armed: State::new(false), picked: State::new(None), on_pick: None, stay_armed: false }
950 }
951
952 pub fn on_pick(mut self, handler: impl Fn(Color) + 'static) -> Self {
954 self.on_pick = Some(Rc::new(handler));
955 self
956 }
957
958 pub fn stay_armed(mut self, stay: bool) -> Self {
960 self.stay_armed = stay;
961 self
962 }
963
964 pub fn is_armed(&self) -> bool {
966 self.armed.get()
967 }
968
969 pub fn set_armed(&self, armed: bool) {
971 self.armed.set_if_changed(armed);
972 }
973
974 pub fn picked(&self) -> Option<Color> {
976 self.picked.get()
977 }
978
979 pub fn pick(&self, color: Color) -> bool {
982 if !self.armed.get() {
983 return false;
984 }
985 self.picked.set(Some(color));
986 if !self.stay_armed {
987 self.armed.set(false);
988 }
989 if let Some(handler) = &self.on_pick {
990 handler(color);
991 }
992 true
993 }
994
995 fn listen_for_escape(&self, cx: &mut BuildCx<'_>) {
997 if self.armed.get() {
998 let armed = self.armed.clone();
999 cx.shortcut(Key::Escape, false, Rc::new(move || armed.set(false)));
1000 }
1001 }
1002}
1003
1004#[derive(Debug)]
1019#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
1020pub struct Eyedropper {
1021 state: EyedropperState,
1022 icon: IconName,
1023 size: ButtonSize,
1024 disabled: bool,
1025 label: Option<String>,
1026 hint: Option<String>,
1027}
1028
1029#[cfg_attr(feature = "live", ravenrs_live::expose)]
1030impl Eyedropper {
1031 pub fn new(state: &EyedropperState) -> Self {
1033 Self {
1034 state: state.clone(),
1035 icon: IconName::Crosshair,
1036 size: ButtonSize::Small,
1037 disabled: false,
1038 label: None,
1039 hint: None,
1040 }
1041 }
1042
1043 pub fn icon(mut self, icon: IconName) -> Self {
1045 self.icon = icon;
1046 self
1047 }
1048
1049 pub fn size(mut self, size: ButtonSize) -> Self {
1051 self.size = size;
1052 self
1053 }
1054
1055 pub fn disabled(mut self, disabled: bool) -> Self {
1057 self.disabled = disabled;
1058 self
1059 }
1060
1061 pub fn label(mut self, label: impl Into<String>) -> Self {
1063 self.label = Some(label.into());
1064 self
1065 }
1066
1067 pub fn hint(mut self, hint: impl Into<String>) -> Self {
1069 self.hint = Some(hint.into());
1070 self
1071 }
1072}
1073
1074impl View for Eyedropper {
1075 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
1076 let labels = cx.labels();
1077 self.state.listen_for_escape(cx);
1078 let armed = self.state.armed.clone();
1079 let is_armed = armed.get() && !self.disabled;
1080 let button = IconButton::new(self.icon)
1081 .label(self.label.clone().unwrap_or_else(|| labels.eyedropper.clone()))
1082 .size(self.size)
1083 .disabled(self.disabled)
1084 .selected(is_armed)
1085 .on_click(move || armed.update(|a| *a = !*a));
1086 let hint = self.hint.clone().unwrap_or_else(|| labels.eyedropper_hint.clone());
1087 cx.child(&Tooltip::new(button, hint))
1088 }
1089}
1090
1091#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
1097pub struct EyedropperArea {
1098 state: EyedropperState,
1099 sampler: Rc<dyn Fn(Point) -> Option<Color>>,
1100 frame: OverlayBox,
1101 label: Option<String>,
1102}
1103
1104impl fmt::Debug for EyedropperArea {
1105 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1106 f.debug_struct("EyedropperArea").field("state", &self.state).finish_non_exhaustive()
1107 }
1108}
1109
1110#[cfg_attr(feature = "live", ravenrs_live::expose)]
1111impl EyedropperArea {
1112 pub fn new(state: &EyedropperState, sampler: impl Fn(Point) -> Option<Color> + 'static) -> Self {
1114 Self { state: state.clone(), sampler: Rc::new(sampler), frame: OverlayBox::default(), label: None }
1115 }
1116
1117 pub fn label(mut self, label: impl Into<String>) -> Self {
1120 self.label = Some(label.into());
1121 self
1122 }
1123}
1124
1125overlay_builders!(EyedropperArea, frame);
1126
1127impl View for EyedropperArea {
1128 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
1129 let labels = cx.labels();
1130 self.state.listen_for_escape(cx);
1131 let (root, surface) = self.frame.build(cx, Paint::None);
1132 if !self.state.armed.get() {
1133 return root;
1134 }
1135 let name = self.label.clone().unwrap_or_else(|| labels.eyedropper_area.clone());
1136 cx.semantics(surface, Semantics::new(Role::Group).name(name));
1137 cx.cursor(surface, CursorIcon::Pointer);
1138 let (state, sampler, space) = (self.state.clone(), self.sampler.clone(), self.frame.space.clone());
1139 cx.on_drag(
1140 surface,
1141 Rc::new(move |d| {
1142 if d.phase != DragPhase::Start {
1143 return;
1144 }
1145 let map = space.map(d.frame);
1146 if !map.placed.contains(d.position) {
1147 return;
1148 }
1149 if let Some(color) = sampler(map.content(d.position)) {
1150 state.pick(color);
1151 }
1152 }),
1153 );
1154 root
1155 }
1156}