1use std::cell::{Cell, RefCell};
6use std::collections::HashMap;
7use std::fmt;
8use std::rc::Rc;
9
10use ravenrs_animation::Spring;
11use ravenrs_core::{
12 Color, CursorIcon, ImageData, ImageQuad, Key, PathElement, PathFill, Point, Quad, Rect, Role, Scene,
13 Size, monotone_slopes,
14};
15use ravenrs_layout::{Align, Axis, Justify, LayoutStyle};
16use ravenrs_reactive::{Binding, Observer, State};
17use ravenrs_style::{Semantic, TextStyle};
18
19use crate::View;
20use crate::color_picker::Hsv;
21use crate::context::{BuildCx, Drag, DragPhase, NodeId, Paint, PaintCx, QuadPaint, Semantics};
22use crate::util;
23use crate::{Button, ButtonSize, ButtonStyle, IconButton, IconName};
24
25mod overlays;
26mod transform;
27mod viewing;
28
29pub use overlays::{
30 CompositionGuide, ContentSpace, Eyedropper, EyedropperArea, EyedropperState, Marquee, RubberBand,
31 RuleOfThirdsGrid,
32};
33pub use transform::{BoxHandle, BoxTransform, CropArea, CropOverlay, Protractor, TransformHandles};
34pub use viewing::{CompareMode, CompareSide, CompareView, ImageLightbox, LightboxItem};
35
36#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
42#[non_exhaustive]
43#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
44pub enum HistogramScale {
45 #[default]
47 Linear,
48 Logarithmic,
51}
52
53#[derive(Clone, Debug, PartialEq)]
55#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
56pub struct HistogramChannel {
57 label: String,
58 color: Color,
59 bins: Rc<[f32]>,
60 line: bool,
61}
62
63#[cfg_attr(feature = "live", ravenrs_live::expose)]
64impl HistogramChannel {
65 pub fn new(
67 label: impl Into<String>,
68 color: Color,
69 bins: impl IntoIterator<Item = impl Into<f64>>,
70 ) -> Self {
71 let bins = bins.into_iter().map(|v| v.into().max(0.0) as f32).collect();
72 Self { label: label.into(), color, bins, line: false }
73 }
74
75 pub fn line(mut self) -> Self {
77 self.line = true;
78 self
79 }
80
81 fn share(&self, bin: Option<&f32>) -> f32 {
83 let total: f32 = self.bins.iter().sum();
84 if total > 0.0 { bin.copied().unwrap_or(0.0) / total } else { 0.0 }
85 }
86}
87
88fn light_sum(colors: impl Iterator<Item = Color>, dim: f32) -> Color {
92 let (mut r, mut g, mut b, mut count) = (0.0_f32, 0.0_f32, 0.0_f32, 0);
93 for c in colors {
94 (r, g, b, count) = (r + c.r, g + c.g, b + c.b, count + 1);
95 }
96 let shade = if count > 1 { dim } else { 1.0 };
97 Color::rgb(r.min(1.0) * shade, g.min(1.0) * shade, b.min(1.0) * shade)
98}
99
100fn overlap_dim(background: Color) -> f32 {
102 let brightness = 0.2126 * background.r + 0.7152 * background.g + 0.0722 * background.b;
103 if brightness > 0.5 { 0.62 } else { 0.86 }
104}
105
106#[derive(Clone, Debug)]
108struct HistogramPicture {
109 channels: Rc<[HistogramChannel]>,
110 scale: HistogramScale,
111 dim: f32,
112 opacity: f32,
113}
114
115#[cfg_attr(feature = "live", ravenrs_live::expose)]
116impl HistogramPicture {
117 fn peak(&self) -> f32 {
120 let top = |inner: bool| {
121 self.channels
122 .iter()
123 .flat_map(|c| {
124 let bins = &c.bins[..];
125 if inner && bins.len() > 2 { &bins[1..bins.len() - 1] } else { bins }
126 })
127 .copied()
128 .fold(0.0, f32::max)
129 };
130 let inner = top(true);
131 if inner > 0.0 { inner } else { top(false) }
132 }
133
134 fn height(&self, value: f32, peak: f32) -> f32 {
135 if peak <= 0.0 {
136 return 0.0;
137 }
138 let h = match self.scale {
139 HistogramScale::Linear => value / peak,
140 HistogramScale::Logarithmic => (1.0 + value).ln() / (1.0 + peak).ln(),
141 };
142 h.clamp(0.0, 1.0)
143 }
144
145 fn column(&self, bins: &[f32], column: usize, columns: usize, peak: f32) -> f32 {
147 let from = column * bins.len() / columns;
148 let to = ((column + 1) * bins.len()).div_ceil(columns).clamp(from + 1, bins.len().max(from + 1));
149 let top = bins.get(from..to).map_or(0.0, |part| part.iter().copied().fold(0.0, f32::max));
150 self.height(top, peak)
151 }
152
153 fn paint(&self, scene: &mut Scene, area: Rect) {
154 let widest = self.channels.iter().map(|c| c.bins.len()).max().unwrap_or(0);
155 if widest == 0 || area.is_empty() {
156 return;
157 }
158 let columns = widest.min(area.width().floor().max(1.0) as usize);
160 let peak = self.peak();
161 let edge = |column: usize| area.x() + (area.width() * column as f32 / columns as f32).round();
162 let mut stack: Vec<(f32, Color)> = Vec::with_capacity(self.channels.len());
163 for column in 0..columns {
164 stack.clear();
165 stack.extend(
166 self.channels
167 .iter()
168 .filter(|c| !c.line && !c.bins.is_empty())
169 .map(|c| (self.column(&c.bins, column, columns, peak), c.color)),
170 );
171 stack.sort_by(|a, b| b.0.total_cmp(&a.0));
172 let (x, right) = (edge(column), edge(column + 1));
173 for i in 0..stack.len() {
175 let (top, bottom) = (stack[i].0, stack.get(i + 1).map_or(0.0, |s| s.0));
176 if top - bottom <= 0.0 {
177 continue;
178 }
179 let color = light_sum(stack[..=i].iter().map(|s| s.1), self.dim);
180 let y = area.max_y() - area.height() * top;
181 let h = area.height() * (top - bottom);
182 scene.push_quad(Quad::new(
183 Rect::new(x, y, (right - x).max(1.0), h),
184 color.with_alpha(self.opacity),
185 ));
186 }
187 }
188 for channel in self.channels.iter().filter(|c| c.line && !c.bins.is_empty()) {
189 let line: Vec<Point> = (0..columns)
190 .map(|column| {
191 let h = self.column(&channel.bins, column, columns, peak);
192 let x = area.x() + area.width() * (column as f32 + 0.5) / columns as f32;
193 Point::new(x, area.max_y() - area.height() * h)
194 })
195 .collect();
196 let color = channel.color.with_alpha(self.opacity);
197 scene.push_path(PathFill::solid(stroke_outline(&line, 1.25), color));
198 }
199 }
200}
201
202fn stroke_outline(points: &[Point], width: f32) -> Vec<PathElement> {
204 let half = width / 2.0;
205 let normal = |i: usize| {
206 let (a, b) = (points[i.saturating_sub(1)], points[(i + 1).min(points.len() - 1)]);
207 let (dx, dy) = (b.x - a.x, b.y - a.y);
208 let length = dx.hypot(dy).max(f32::EPSILON);
209 (-dy / length * half, dx / length * half)
210 };
211 let mut outline = Vec::with_capacity(points.len() * 2 + 2);
212 for (i, p) in points.iter().enumerate() {
213 let (nx, ny) = normal(i);
214 let at = Point::new(p.x + nx, p.y + ny);
215 outline.push(if i == 0 { PathElement::MoveTo(at) } else { PathElement::LineTo(at) });
216 }
217 for (i, p) in points.iter().enumerate().rev() {
218 let (nx, ny) = normal(i);
219 outline.push(PathElement::LineTo(Point::new(p.x - nx, p.y - ny)));
220 }
221 outline.push(PathElement::Close);
222 outline
223}
224
225#[derive(Clone, Debug)]
239#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
240pub struct Histogram {
241 channels: Rc<[HistogramChannel]>,
242 scale: HistogramScale,
243 clip_share: f32,
244 zones: u32,
245 width: Option<f32>,
246 height: f32,
247 label: Option<String>,
248}
249
250#[cfg_attr(feature = "live", ravenrs_live::expose)]
251impl Histogram {
252 pub fn new(channels: impl IntoIterator<Item = HistogramChannel>) -> Self {
254 Self {
255 channels: channels.into_iter().collect(),
256 scale: HistogramScale::Linear,
257 clip_share: 0.001,
258 zones: 0,
259 width: None,
260 height: 96.0,
261 label: None,
262 }
263 }
264
265 pub fn scale(mut self, scale: HistogramScale) -> Self {
267 self.scale = scale;
268 self
269 }
270
271 pub fn clip_share(mut self, share: f32) -> Self {
274 self.clip_share = share;
275 self
276 }
277
278 pub fn zones(mut self, zones: u32) -> Self {
280 self.zones = zones;
281 self
282 }
283
284 pub fn size(mut self, width: f32, height: f32) -> Self {
286 self.width = Some(width);
287 self.height = height;
288 self
289 }
290
291 pub fn height(mut self, height: f32) -> Self {
293 self.height = height;
294 self
295 }
296
297 pub fn label(mut self, label: impl Into<String>) -> Self {
299 self.label = Some(label.into());
300 self
301 }
302
303 fn clipped(&self, dim: f32) -> (Option<Color>, Option<Color>) {
305 let end = |pick: fn(&[f32]) -> Option<&f32>| {
306 let over: Vec<Color> = self
307 .channels
308 .iter()
309 .filter(|c| !c.line && c.share(pick(&c.bins)) > self.clip_share)
310 .map(|c| c.color)
311 .collect();
312 (!over.is_empty()).then(|| light_sum(over.into_iter(), dim))
313 };
314 (end(<[f32]>::first), end(<[f32]>::last))
315 }
316}
317
318impl View for Histogram {
319 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
320 let theme = *cx.theme();
321 let labels = cx.labels();
322 let background = theme.color(Semantic::Canvas);
323 let dim = overlap_dim(background);
324 let picture =
325 HistogramPicture { channels: self.channels.clone(), scale: self.scale, dim, opacity: 1.0 };
326 let (low, high) = self.clipped(dim);
327 let (border, radius, zones) = (theme.color(Semantic::Border), theme.radii.sm, self.zones);
328 let node = cx.container(
329 LayoutStyle {
330 width: self.width,
331 height: Some(self.height),
332 align_self: Some(if self.width.is_some() { Align::Start } else { Align::Stretch }),
333 ..LayoutStyle::leaf()
334 },
335 &[],
336 Paint::Custom(Rc::new(move |frame: Rect, scene| {
337 scene.push_quad(Quad::new(frame, background).radius(radius).border(1.0, border));
338 let area = frame.inflate(-1.0);
339 for zone in 1..zones {
340 let x = area.x() + (area.width() * zone as f32 / zones as f32).round();
341 let line = Rect::new(x, area.y(), 1.0, area.height());
342 scene.push_quad(Quad::new(line, border.with_alpha(border.a * 0.5)));
343 }
344 picture.paint(scene, area);
345 paint_clip_marks(scene, area, low, high);
346 })),
347 );
348 let mut value: Vec<&str> = self.channels.iter().map(|c| c.label.as_str()).collect();
349 if low.is_some() {
350 value.push(&labels.histogram_clipped_low);
351 }
352 if high.is_some() {
353 value.push(&labels.histogram_clipped_high);
354 }
355 let name = self.label.clone().unwrap_or_else(|| labels.histogram.clone());
356 cx.semantics(node, Semantics::new(Role::Image).name(name).value(value.join(", ")));
357 node
358 }
359}
360
361fn paint_clip_marks(scene: &mut Scene, area: Rect, low: Option<Color>, high: Option<Color>) {
363 const SIDE: f32 = 9.0;
364 let (top, left, right) = (area.y() + 2.0, area.x() + 2.0, area.max_x() - 2.0);
365 if let Some(color) = low {
366 let corner = [(left, top), (left + SIDE, top), (left, top + SIDE)];
367 scene.push_path(PathFill::solid(triangle(corner), color));
368 }
369 if let Some(color) = high {
370 let corner = [(right, top), (right - SIDE, top), (right, top + SIDE)];
371 scene.push_path(PathFill::solid(triangle(corner), color));
372 }
373}
374
375fn triangle(corners: [(f32, f32); 3]) -> Vec<PathElement> {
376 let at = |(x, y): (f32, f32)| Point::new(x, y);
377 vec![
378 PathElement::MoveTo(at(corners[0])),
379 PathElement::LineTo(at(corners[1])),
380 PathElement::LineTo(at(corners[2])),
381 PathElement::Close,
382 ]
383}
384
385const RULER_TICKS_PER_PIXEL: f32 = 0.5;
392
393fn round_step(least: f32) -> f32 {
395 if !least.is_finite() || least <= 0.0 {
396 return 1.0;
397 }
398 let power = 10_f32.powf(least.log10().floor());
399 [1.0, 2.0, 5.0, 10.0].into_iter().map(|m| m * power).find(|s| *s >= least * 0.999).unwrap_or(10.0 * power)
400}
401
402fn ruler_subdivisions(step: f32, pixels_per_unit: f32, whole_units: bool) -> u32 {
404 [10_u32, 5, 2]
405 .into_iter()
406 .find(|&n| {
407 let part = step / n as f32;
408 part * pixels_per_unit >= 4.0 && (!whole_units || part >= 1.0)
409 })
410 .unwrap_or(1)
411}
412
413fn ruler_number(value: f32, step: f32) -> String {
415 let decimals = if step >= 1.0 { 0 } else { (-step.log10()).ceil() as usize };
416 let text = format!("{value:.decimals$}");
417 if text.starts_with('-') && text[1..].chars().all(|c| c == '0' || c == '.') {
419 text[1..].to_string()
420 } else {
421 text
422 }
423}
424
425#[derive(Clone, Debug)]
437#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
438pub struct Ruler {
439 axis: Axis,
440 scale: f32,
441 origin: f32,
442 marker: Option<f32>,
443 thickness: f32,
444 min_spacing: f32,
445 whole_units: bool,
446 label: Option<String>,
447}
448
449#[cfg_attr(feature = "live", ravenrs_live::expose)]
450impl Ruler {
451 pub fn horizontal() -> Self {
453 Self::along(Axis::Horizontal)
454 }
455
456 pub fn vertical() -> Self {
458 Self::along(Axis::Vertical)
459 }
460
461 fn along(axis: Axis) -> Self {
462 Self {
463 axis,
464 scale: 1.0,
465 origin: 0.0,
466 marker: None,
467 thickness: 18.0,
468 min_spacing: 64.0,
469 whole_units: false,
470 label: None,
471 }
472 }
473
474 pub fn scale(mut self, pixels_per_unit: f32) -> Self {
476 self.scale = pixels_per_unit;
477 self
478 }
479
480 pub fn origin(mut self, units: f32) -> Self {
482 self.origin = units;
483 self
484 }
485
486 pub fn marker(mut self, units: f32) -> Self {
488 self.marker = Some(units);
489 self
490 }
491
492 pub fn thickness(mut self, thickness: f32) -> Self {
494 self.thickness = thickness;
495 self
496 }
497
498 pub fn min_spacing(mut self, pixels: f32) -> Self {
500 self.min_spacing = pixels;
501 self
502 }
503
504 pub fn whole_units(mut self) -> Self {
506 self.whole_units = true;
507 self
508 }
509
510 pub fn label(mut self, label: impl Into<String>) -> Self {
512 self.label = Some(label.into());
513 self
514 }
515
516 fn step(&self) -> f32 {
518 let least = self.min_spacing.max(1.0) / self.scale.max(f32::EPSILON);
519 round_step(if self.whole_units { least.max(1.0) } else { least })
520 }
521}
522
523impl View for Ruler {
524 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
525 let theme = *cx.theme();
526 let labels = cx.labels();
527 let horizontal = self.axis == Axis::Horizontal;
528 let (surface, tick, ink, accent) = (
529 theme.color(Semantic::Surface),
530 theme.color(Semantic::Border),
531 theme.color(Semantic::InkMuted),
532 theme.color(Semantic::Accent),
533 );
534 let attrs = {
535 let mut attrs = util::attrs(&theme, TextStyle::Caption, None);
536 attrs.size = 9.0;
537 attrs.line_height = 10.0;
538 attrs
539 };
540 let ruler = self.clone();
541 let step = self.step();
542 let node = cx.container(
543 LayoutStyle {
544 width: (!horizontal).then_some(self.thickness),
545 height: horizontal.then_some(self.thickness),
546 align_self: Some(Align::Stretch),
547 overflow_hidden: true,
548 ..LayoutStyle::leaf()
549 },
550 &[],
551 Paint::Rich(Rc::new(move |frame: Rect, paint: &mut PaintCx<'_>| {
552 if frame.is_empty() {
553 return;
554 }
555 let scene = &mut *paint.scene;
556 scene.push_quad(Quad::new(frame, surface));
557 let edge = if horizontal {
559 Rect::new(frame.x(), frame.max_y() - 1.0, frame.width(), 1.0)
560 } else {
561 Rect::new(frame.max_x() - 1.0, frame.y(), 1.0, frame.height())
562 };
563 scene.push_quad(Quad::new(edge, tick));
564 scene.push_clip(frame);
565 let length = if horizontal { frame.width() } else { frame.height() };
566 let across = if horizontal { frame.height() } else { frame.width() };
567 let place = |units: f32| (units - ruler.origin) * ruler.scale;
568 let parts = ruler_subdivisions(step, ruler.scale, ruler.whole_units);
569 let part = step / parts as f32;
570 let first = (ruler.origin / part).floor() as i64;
571 let limit = (length * RULER_TICKS_PER_PIXEL) as i64 + 2;
572 for index in first..first.saturating_add(limit) {
573 let units = index as f32 * part;
574 let at = place(units);
575 if at > length {
576 break;
577 }
578 let major = index.rem_euclid(i64::from(parts)) == 0;
579 let half =
580 parts.is_multiple_of(2) && index.rem_euclid(i64::from(parts)) == i64::from(parts / 2);
581 let size = if major {
582 across
583 } else if half {
584 across * 0.45
585 } else {
586 across * 0.25
587 };
588 let mark = if horizontal {
589 Rect::new(frame.x() + at.round(), frame.max_y() - size, 1.0, size)
590 } else {
591 Rect::new(frame.max_x() - size, frame.y() + at.round(), size, 1.0)
592 };
593 paint.scene.push_quad(Quad::new(mark, tick));
594 if !major {
595 continue;
596 }
597 let number = ruler_number(units, step);
598 if horizontal {
599 let origin = Point::new(frame.x() + at.round() + 3.0, frame.y() + 1.0);
600 let mut layout = paint.text.layout(&number, &attrs);
601 layout.measure(None);
602 layout.paint(origin, ink, paint.scene);
603 } else {
604 let mut y = frame.y() + at.round() + 2.0;
606 for digit in number.chars() {
607 let mut buffer = [0_u8; 4];
608 let mut layout = paint.text.layout(digit.encode_utf8(&mut buffer), &attrs);
609 let width = layout.measure(None).width;
610 let x = frame.x() + ((across - 3.0 - width) / 2.0).max(0.0);
611 layout.paint(Point::new(x, y), ink, paint.scene);
612 y += attrs.line_height - 1.0;
613 }
614 }
615 }
616 if let Some(marker) = ruler.marker {
617 let at = place(marker);
618 let line = if horizontal {
619 Rect::new(frame.x() + at - 0.5, frame.y(), 1.0, frame.height())
620 } else {
621 Rect::new(frame.x(), frame.y() + at - 0.5, frame.width(), 1.0)
622 };
623 paint.scene.push_quad(Quad::new(line, accent));
624 let corners = if horizontal {
625 let (x, y) = (frame.x() + at, frame.max_y());
626 [(x - 4.0, y - 5.0), (x + 4.0, y - 5.0), (x, y)]
627 } else {
628 let (x, y) = (frame.max_x(), frame.y() + at);
629 [(x - 5.0, y - 4.0), (x - 5.0, y + 4.0), (x, y)]
630 };
631 paint.scene.push_path(PathFill::solid(triangle(corners), accent));
632 }
633 paint.scene.pop_clip();
634 })),
635 );
636 let name = self.label.clone().unwrap_or_else(|| labels.ruler.clone());
637 let mut semantics = Semantics::new(Role::Image).name(name);
638 if let Some(marker) = self.marker {
639 semantics = semantics.value(ruler_number(marker, step));
640 }
641 cx.semantics(node, semantics);
642 node
643 }
644}
645
646#[derive(Clone, Copy, Debug, Default, PartialEq)]
652#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
653pub struct HueSaturation {
654 pub hue: f32,
656 pub saturation: f32,
658}
659
660#[cfg_attr(feature = "live", ravenrs_live::expose)]
661impl HueSaturation {
662 pub fn new(hue: f32, saturation: f32) -> Self {
664 Self { hue: hue.rem_euclid(360.0), saturation: saturation.clamp(0.0, 1.0) }
665 }
666
667 fn color(self, value: f32) -> Color {
669 Hsv { hue: self.hue, saturation: self.saturation, value, alpha: 1.0 }.color()
670 }
671
672 fn offset(self, radius: f32) -> Point {
675 let (sin, cos) = self.hue.to_radians().sin_cos();
676 Point::new(cos * self.saturation * radius, -sin * self.saturation * radius)
677 }
678
679 fn at(offset: Point, radius: f32) -> Self {
682 let distance = offset.x.hypot(offset.y);
683 let hue = (-offset.y).atan2(offset.x).to_degrees();
684 Self::new(hue, distance / radius.max(1.0))
685 }
686}
687
688const WHEEL_MARGIN: f32 = 7.0;
690
691thread_local! {
692 static WHEEL_DISCS: RefCell<HashMap<(u32, u32), ImageData>> = RefCell::new(HashMap::new());
695}
696
697fn wheel_disc(side: u32, value: f32) -> Option<ImageData> {
700 WHEEL_DISCS.with_borrow_mut(|discs| {
701 let key = (side, value.to_bits());
702 if let Some(disc) = discs.get(&key) {
703 return Some(disc.clone());
704 }
705 if discs.len() >= 8 {
706 discs.clear();
707 }
708 let middle = side as f32 / 2.0;
709 let disc = ImageData::from_fn(side, side, |x, y| {
710 let offset = Point::new(x as f32 + 0.5 - middle, y as f32 + 0.5 - middle);
711 let coverage = (middle - offset.x.hypot(offset.y)).clamp(0.0, 1.0);
712 HueSaturation::at(offset, middle).color(value).with_alpha(coverage)
713 })?;
714 discs.insert(key, disc.clone());
715 Some(disc)
716 })
717}
718
719#[derive(Debug)]
731#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
732pub struct ColorWheel {
733 value: Binding<HueSaturation>,
734 diameter: f32,
735 brightness: f32,
736 label: Option<String>,
737 disabled: bool,
738}
739
740#[cfg_attr(feature = "live", ravenrs_live::expose)]
741impl ColorWheel {
742 pub fn new(value: Binding<HueSaturation>) -> Self {
744 Self { value, diameter: 160.0, brightness: 1.0, label: None, disabled: false }
745 }
746
747 pub fn diameter(mut self, diameter: f32) -> Self {
749 self.diameter = diameter;
750 self
751 }
752
753 pub fn brightness(mut self, brightness: f32) -> Self {
755 self.brightness = (brightness.clamp(0.0, 1.0) * 16.0).round() / 16.0;
757 self
758 }
759
760 pub fn label(mut self, label: impl Into<String>) -> Self {
762 self.label = Some(label.into());
763 self
764 }
765
766 pub fn disabled(mut self, disabled: bool) -> Self {
768 self.disabled = disabled;
769 self
770 }
771}
772
773impl View for ColorWheel {
774 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
775 let theme = *cx.theme();
776 let labels = cx.labels();
777 let current = self.value.get();
778 let (edge, ink, surface) =
779 (theme.color(Semantic::Border), theme.color(Semantic::Ink), theme.color(Semantic::Surface));
780 let (value, opacity) = (self.brightness, if self.disabled { 0.5 } else { 1.0 });
781 let node = cx.container(
782 LayoutStyle {
783 width: Some(self.diameter),
784 height: Some(self.diameter),
785 align_self: Some(Align::Start),
786 ..LayoutStyle::leaf()
787 },
788 &[],
789 Paint::Rich(Rc::new(move |frame: Rect, paint: &mut PaintCx<'_>| {
790 let disc_frame = frame.inflate(-WHEEL_MARGIN);
791 if disc_frame.is_empty() {
792 return;
793 }
794 let side = (disc_frame.width() * paint.scale).ceil() as u32;
795 let scene = &mut *paint.scene;
796 if let Some(disc) = wheel_disc(side, value) {
797 scene.push_image(ImageQuad { opacity, ..ImageQuad::new(disc_frame, disc) });
798 }
799 let radius = disc_frame.width() / 2.0;
800 let ring = Quad::new(disc_frame.inflate(0.5), Color::TRANSPARENT).radius(radius + 0.5);
801 scene.push_quad(ring.border(1.0, edge));
802 let middle = frame.center();
803 let tick = |w: f32, h: f32| Rect::new(middle.x - w / 2.0, middle.y - h / 2.0, w, h);
804 let cross = HueSaturation::default().color(value * 0.45).with_alpha(opacity);
806 scene.push_quad(Quad::new(tick(9.0, 1.0), cross));
807 scene.push_quad(Quad::new(tick(1.0, 9.0), cross));
808 let offset = current.offset(radius);
810 let puck = Rect::new(middle.x + offset.x - 6.5, middle.y + offset.y - 6.5, 13.0, 13.0);
811 scene.push_quad(
812 Quad::new(puck, current.color(value).with_alpha(opacity))
813 .radius(6.5)
814 .border(2.0, surface),
815 );
816 scene
817 .push_quad(Quad::new(puck.inflate(1.0), Color::TRANSPARENT).radius(7.5).border(1.0, ink));
818 })),
819 );
820 let name = self.label.clone().unwrap_or_else(|| labels.color_wheel.clone());
821 let text = format!(
822 "{} {}°, {} {}%",
823 labels.color_hue,
824 current.hue.round(),
825 labels.color_saturation,
826 (current.saturation * 100.0).round()
827 );
828 cx.semantics(node, Semantics::new(Role::Slider).name(name).value(text).disabled(self.disabled));
829 if self.disabled {
830 return node;
831 }
832 cx.focusable(node, self.diameter / 2.0);
833 cx.cursor(node, CursorIcon::Pointer);
834 let (dragged, modifiers) = (self.value.clone(), cx.modifiers());
835 cx.on_drag(
836 node,
837 Rc::new(move |d| {
838 if d.phase == DragPhase::End {
839 return;
840 }
841 let middle = d.frame.center();
842 let radius = (d.frame.width() / 2.0 - WHEEL_MARGIN).max(1.0);
843 let offset = Point::new(d.position.x - middle.x, d.position.y - middle.y);
844 let picked = HueSaturation::at(offset, radius);
845 let hue = if modifiers.get().shift { dragged.get().hue } else { picked.hue };
846 dragged.set(HueSaturation::new(hue, picked.saturation));
847 }),
848 );
849 let reset = self.value.clone();
850 cx.on_double_click(node, Rc::new(move || reset.update(|v| v.saturation = 0.0)));
851 let keyed = self.value.clone();
852 cx.on_key(
853 node,
854 Rc::new(move |key, modifiers| {
855 let times = if modifiers.shift { 10.0 } else { 1.0 };
856 let now = keyed.get();
857 keyed.set(match key {
858 Key::ArrowLeft => HueSaturation::new(now.hue - times, now.saturation),
859 Key::ArrowRight => HueSaturation::new(now.hue + times, now.saturation),
860 Key::ArrowUp => HueSaturation::new(now.hue, now.saturation + times / 100.0),
861 Key::ArrowDown => HueSaturation::new(now.hue, now.saturation - times / 100.0),
862 Key::Home => HueSaturation::new(now.hue, 0.0),
863 _ => return false,
864 });
865 true
866 }),
867 );
868 node
869 }
870}
871
872const CURVE_MARGIN: f32 = 7.0;
878const CURVE_REACH: f32 = 11.0;
880const CURVE_MIN_GAP: f32 = 0.01;
882
883fn curve_points(points: &[Point]) -> Vec<Point> {
885 let mut sorted: Vec<Point> = points
886 .iter()
887 .filter(|p| p.x.is_finite() && p.y.is_finite())
888 .map(|p| Point::new(p.x.clamp(0.0, 1.0), p.y.clamp(0.0, 1.0)))
889 .collect();
890 sorted.sort_by(|a, b| a.x.total_cmp(&b.x));
891 sorted.dedup_by(|a, b| (a.x - b.x).abs() < f32::EPSILON);
892 if sorted.len() < 2 { vec![Point::ZERO, Point::new(1.0, 1.0)] } else { sorted }
893}
894
895#[derive(Clone, Copy)]
897struct CurvePlot(Rect);
898
899#[cfg_attr(feature = "live", ravenrs_live::expose)]
900impl CurvePlot {
901 fn of(frame: Rect) -> Self {
902 Self(frame.inflate(-CURVE_MARGIN))
903 }
904
905 fn screen(self, p: Point) -> Point {
906 Point::new(self.0.x() + p.x * self.0.width(), self.0.max_y() - p.y * self.0.height())
907 }
908
909 fn value(self, at: Point) -> Point {
910 Point::new(
911 ((at.x - self.0.x()) / self.0.width().max(1.0)).clamp(0.0, 1.0),
912 ((self.0.max_y() - at.y) / self.0.height().max(1.0)).clamp(0.0, 1.0),
913 )
914 }
915
916 fn handle_at(self, points: &[Point], at: Point) -> Option<usize> {
918 let distance = |p: &Point| {
919 let s = self.screen(*p);
920 (s.x - at.x).hypot(s.y - at.y)
921 };
922 points
923 .iter()
924 .enumerate()
925 .filter(|(_, p)| distance(p) <= CURVE_REACH)
926 .min_by(|a, b| distance(a.1).total_cmp(&distance(b.1)))
927 .map(|(i, _)| i)
928 }
929}
930
931fn curve_move(points: &mut [Point], index: usize, to: Point) {
933 let least = if index == 0 { 0.0 } else { points[index - 1].x + CURVE_MIN_GAP };
934 let most = points.get(index + 1).map_or(1.0, |next| next.x - CURVE_MIN_GAP);
935 if let Some(point) = points.get_mut(index) {
936 *point = Point::new(to.x.clamp(least, most.max(least)), to.y.clamp(0.0, 1.0));
937 }
938}
939
940fn curve_insert(points: &mut Vec<Point>, at: Point) -> Option<usize> {
942 if points.iter().any(|p| (p.x - at.x).abs() < CURVE_MIN_GAP) {
943 return None;
944 }
945 let index = points.iter().position(|p| p.x > at.x).unwrap_or(points.len());
946 points.insert(index, at);
947 Some(index)
948}
949
950#[derive(Debug)]
968#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
969pub struct CurveEditor {
970 points: Binding<Vec<Point>>,
971 width: Option<f32>,
972 height: f32,
973 color: Option<Color>,
974 baseline: bool,
975 grid: u32,
976 backdrop: Vec<HistogramChannel>,
977 backdrop_scale: HistogramScale,
978 label: Option<String>,
979 disabled: bool,
980}
981
982#[cfg_attr(feature = "live", ravenrs_live::expose)]
983impl CurveEditor {
984 pub fn new(points: Binding<Vec<Point>>) -> Self {
986 Self {
987 points,
988 width: None,
989 height: 200.0,
990 color: None,
991 baseline: true,
992 grid: 4,
993 backdrop: Vec::new(),
994 backdrop_scale: HistogramScale::Linear,
995 label: None,
996 disabled: false,
997 }
998 }
999
1000 pub fn value_at(points: &[Point], x: f32) -> f32 {
1003 let points = curve_points(points);
1004 let slopes = monotone_slopes(&points);
1005 curve_value(&points, &slopes, x)
1006 }
1007
1008 pub fn size(mut self, width: f32, height: f32) -> Self {
1010 self.width = Some(width);
1011 self.height = height;
1012 self
1013 }
1014
1015 pub fn height(mut self, height: f32) -> Self {
1017 self.height = height;
1018 self
1019 }
1020
1021 pub fn color(mut self, color: Color) -> Self {
1023 self.color = Some(color);
1024 self
1025 }
1026
1027 pub fn baseline(mut self, baseline: bool) -> Self {
1029 self.baseline = baseline;
1030 self
1031 }
1032
1033 pub fn grid(mut self, parts: u32) -> Self {
1035 self.grid = parts;
1036 self
1037 }
1038
1039 pub fn histogram(
1041 mut self,
1042 channels: impl IntoIterator<Item = HistogramChannel>,
1043 scale: HistogramScale,
1044 ) -> Self {
1045 self.backdrop = channels.into_iter().collect();
1046 self.backdrop_scale = scale;
1047 self
1048 }
1049
1050 pub fn label(mut self, label: impl Into<String>) -> Self {
1052 self.label = Some(label.into());
1053 self
1054 }
1055
1056 pub fn disabled(mut self, disabled: bool) -> Self {
1058 self.disabled = disabled;
1059 self
1060 }
1061}
1062
1063fn curve_value(points: &[Point], slopes: &[f32], x: f32) -> f32 {
1065 let x = x.clamp(points[0].x, points[points.len() - 1].x);
1066 let i = points.iter().rposition(|p| p.x <= x).unwrap_or(0).min(points.len() - 2);
1067 let (a, b) = (points[i], points[i + 1]);
1068 let run = (b.x - a.x).max(f32::EPSILON);
1069 let t = (x - a.x) / run;
1070 let (t2, t3) = (t * t, t * t * t);
1071 let value = (2.0 * t3 - 3.0 * t2 + 1.0) * a.y
1072 + (t3 - 2.0 * t2 + t) * run * slopes[i]
1073 + (-2.0 * t3 + 3.0 * t2) * b.y
1074 + (t3 - t2) * run * slopes[i + 1];
1075 value.clamp(0.0, 1.0)
1076}
1077
1078impl View for CurveEditor {
1079 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
1080 let theme = *cx.theme();
1081 let labels = cx.labels();
1082 let points = curve_points(&self.points.get());
1083 let chosen = cx.use_state(|| None::<usize>);
1084 let chosen_now = chosen.get().filter(|i| *i < points.len());
1085 let (background, grid, muted) =
1086 (theme.color(Semantic::Canvas), theme.color(Semantic::Border), theme.color(Semantic::InkMuted));
1087 let (surface, focus) = (theme.color(Semantic::Surface), theme.color(Semantic::Accent));
1088 let opacity = if self.disabled { 0.5 } else { 1.0 };
1089 let curve_color = self.color.unwrap_or_else(|| theme.color(Semantic::Ink)).with_alpha(opacity);
1090 let backdrop = (!self.backdrop.is_empty()).then(|| HistogramPicture {
1091 channels: self.backdrop.iter().cloned().collect(),
1092 scale: self.backdrop_scale,
1093 dim: overlap_dim(background),
1094 opacity: 0.35,
1095 });
1096 let (parts, baseline, radius) = (self.grid, self.baseline, theme.radii.sm);
1097 let drawn = points.clone();
1098 let node = cx.container(
1099 LayoutStyle {
1100 width: self.width,
1101 height: Some(self.height),
1102 align_self: Some(if self.width.is_some() { Align::Start } else { Align::Stretch }),
1103 ..LayoutStyle::leaf()
1104 },
1105 &[],
1106 Paint::Custom(Rc::new(move |frame: Rect, scene| {
1107 scene.push_quad(Quad::new(frame, background).radius(radius).border(1.0, grid));
1108 let plot = CurvePlot::of(frame);
1109 let area = plot.0;
1110 if area.is_empty() {
1111 return;
1112 }
1113 if let Some(backdrop) = &backdrop {
1114 backdrop.paint(scene, area);
1115 }
1116 for part in 0..=parts.max(1) {
1117 let share = part as f32 / parts.max(1) as f32;
1118 let line = grid.with_alpha(grid.a * 0.6);
1119 let x = (area.x() + area.width() * share).round();
1120 let y = (area.y() + area.height() * share).round();
1121 scene.push_quad(Quad::new(Rect::new(x, area.y(), 1.0, area.height()), line));
1122 scene.push_quad(Quad::new(Rect::new(area.x(), y, area.width(), 1.0), line));
1123 }
1124 if baseline {
1125 let line = [plot.screen(Point::ZERO), plot.screen(Point::new(1.0, 1.0))];
1126 scene.push_path(PathFill::solid(stroke_outline(&line, 1.0), muted.with_alpha(0.6)));
1127 }
1128 let slopes = monotone_slopes(&drawn);
1130 let samples = ((area.width() / 2.0).ceil() as usize).max(2);
1131 let (from, to) = (drawn[0].x, drawn[drawn.len() - 1].x);
1132 let mut line: Vec<Point> = Vec::with_capacity(samples + 3);
1133 if from > 0.0 {
1134 line.push(plot.screen(Point::new(0.0, drawn[0].y)));
1135 }
1136 line.extend((0..=samples).map(|i| {
1137 let x = from + (to - from) * i as f32 / samples as f32;
1138 plot.screen(Point::new(x, curve_value(&drawn, &slopes, x)))
1139 }));
1140 if to < 1.0 {
1141 line.push(plot.screen(Point::new(1.0, drawn[drawn.len() - 1].y)));
1142 }
1143 scene.push_path(PathFill::solid(stroke_outline(&line, 1.75), curve_color));
1144 for (i, p) in drawn.iter().enumerate() {
1145 let at = plot.screen(*p);
1146 let (half, fill) = if chosen_now == Some(i) { (6.0, focus) } else { (4.5, surface) };
1148 let handle = Rect::new(at.x - half, at.y - half, half * 2.0, half * 2.0);
1149 scene.push_quad(
1150 Quad::new(handle, fill.with_alpha(opacity)).radius(half).border(1.5, curve_color),
1151 );
1152 }
1153 })),
1154 );
1155 let name = self.label.clone().unwrap_or_else(|| labels.curve_editor.clone());
1156 let percent = |v: f32| (v * 100.0).round();
1157 let value = match chosen_now {
1158 Some(i) => format!(
1159 "{} {}/{}: {} {}%, {} {}%",
1160 labels.curve_point,
1161 i + 1,
1162 points.len(),
1163 labels.curve_input,
1164 percent(points[i].x),
1165 labels.curve_output,
1166 percent(points[i].y),
1167 ),
1168 None => format!("{} {}", points.len(), labels.curve_points),
1169 };
1170 cx.semantics(node, Semantics::new(Role::Slider).name(name).value(value).disabled(self.disabled));
1171 if self.disabled {
1172 return node;
1173 }
1174 cx.focusable(node, theme.radii.sm);
1175 cx.cursor(node, CursorIcon::Pointer);
1176 let press = cx.use_state(|| Rc::new(Cell::new((Point::ZERO, Rect::ZERO)))).get();
1177
1178 let (binding, picked, pressed) = (self.points.clone(), chosen.clone(), press.clone());
1179 cx.on_drag(
1180 node,
1181 Rc::new(move |d| {
1182 let plot = CurvePlot::of(d.frame);
1183 let mut points = curve_points(&binding.get());
1184 match d.phase {
1185 DragPhase::Start => {
1186 pressed.set((d.position, d.frame));
1187 let index = plot.handle_at(&points, d.position).or_else(|| {
1188 let added = curve_insert(&mut points, plot.value(d.position));
1189 if added.is_some() {
1190 binding.set(points.clone());
1191 }
1192 added
1193 });
1194 picked.set(index);
1195 }
1196 DragPhase::Move => {
1197 if let Some(index) = *picked.peek() {
1198 curve_move(&mut points, index, plot.value(d.position));
1199 binding.set(points);
1200 }
1201 }
1202 DragPhase::End => {}
1203 }
1204 }),
1205 );
1206
1207 let (binding, picked) = (self.points.clone(), chosen.clone());
1208 cx.on_double_click(
1209 node,
1210 Rc::new(move || {
1211 let (position, frame) = press.get();
1212 let mut points = curve_points(&binding.get());
1213 let hit = CurvePlot::of(frame).handle_at(&points, position);
1214 if let Some(index) = hit.filter(|i| *i != 0 && *i + 1 != points.len()) {
1215 points.remove(index);
1216 binding.set(points);
1217 picked.set(None);
1218 }
1219 }),
1220 );
1221
1222 let (binding, picked) = (self.points.clone(), chosen);
1223 cx.on_key(
1224 node,
1225 Rc::new(move |key, modifiers| {
1226 let mut points = curve_points(&binding.get());
1227 let current = picked.peek().filter(|i| *i < points.len());
1228 let last = points.len() - 1;
1229 let step = if modifiers.shift { 0.05 } else { 0.01 };
1230 let nudge = |x: f32, y: f32| {
1231 let index = current.unwrap_or(0);
1232 let was = points[index];
1233 (index, Point::new(was.x + x * step, was.y + y * step))
1234 };
1235 let moved = match key {
1236 Key::ArrowLeft => nudge(-1.0, 0.0),
1237 Key::ArrowRight => nudge(1.0, 0.0),
1238 Key::ArrowUp => nudge(0.0, 1.0),
1239 Key::ArrowDown => nudge(0.0, -1.0),
1240 Key::PageUp => {
1241 picked.set(Some(current.map_or(last, |i| i.saturating_sub(1))));
1242 return true;
1243 }
1244 Key::PageDown => {
1245 picked.set(Some(current.map_or(0, |i| (i + 1).min(last))));
1246 return true;
1247 }
1248 Key::Enter => {
1249 let index = current.unwrap_or(0).min(last - 1);
1250 let x = (points[index].x + points[index + 1].x) / 2.0;
1251 let slopes = monotone_slopes(&points);
1252 let at = Point::new(x, curve_value(&points, &slopes, x));
1253 if let Some(added) = curve_insert(&mut points, at) {
1254 binding.set(points);
1255 picked.set(Some(added));
1256 }
1257 return true;
1258 }
1259 Key::Delete | Key::Backspace => {
1260 let Some(index) = current.filter(|i| *i != 0 && *i != last) else {
1261 return false;
1262 };
1263 points.remove(index);
1264 binding.set(points);
1265 picked.set(Some(index.min(last - 1)));
1266 return true;
1267 }
1268 _ => return false,
1269 };
1270 curve_move(&mut points, moved.0, moved.1);
1271 binding.set(points);
1272 picked.set(Some(moved.0));
1273 true
1274 }),
1275 );
1276 node
1277 }
1278}
1279
1280const ZOOM_STOPS: [f32; 17] =
1286 [0.05, 0.1, 0.125, 0.25, 0.33, 0.5, 0.67, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0, 8.0, 16.0, 32.0, 64.0];
1287const ZOOM_DEFAULT_MAX: f32 = 16.0;
1289
1290pub(crate) fn zoom_stop(scale: f32, up: bool) -> f32 {
1292 let next = if up {
1293 ZOOM_STOPS.iter().copied().find(|s| *s > scale * 1.01)
1294 } else {
1295 ZOOM_STOPS.iter().rev().copied().find(|s| *s < scale * 0.99)
1296 };
1297 next.unwrap_or(scale)
1298}
1299
1300#[derive(Clone, Copy, Debug, PartialEq)]
1307#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
1308pub struct ZoomView {
1309 pub scale: f32,
1311 pub center: Point,
1313 pub fit: bool,
1315 pub viewport: Size,
1317}
1318
1319impl Default for ZoomView {
1320 fn default() -> Self {
1321 Self::fit()
1322 }
1323}
1324
1325#[cfg_attr(feature = "live", ravenrs_live::expose)]
1326impl ZoomView {
1327 pub const fn fit() -> Self {
1329 Self { scale: 1.0, center: Point::new(0.5, 0.5), fit: true, viewport: Size::ZERO }
1330 }
1331
1332 pub const fn at(scale: f32) -> Self {
1334 Self { scale, center: Point::new(0.5, 0.5), fit: false, viewport: Size::ZERO }
1335 }
1336
1337 pub const fn centered_on(mut self, center: Point) -> Self {
1339 self.center = center;
1340 self
1341 }
1342
1343 pub fn placed(&self, content: Size) -> Rect {
1345 let (w, h) = (content.width * self.scale, content.height * self.scale);
1346 Rect::new(
1347 self.viewport.width / 2.0 - self.center.x * w,
1348 self.viewport.height / 2.0 - self.center.y * h,
1349 w,
1350 h,
1351 )
1352 }
1353
1354 pub fn visible(&self, content: Size) -> Rect {
1356 if self.viewport.width <= 0.0 || self.viewport.height <= 0.0 {
1358 return Rect::new(0.0, 0.0, 1.0, 1.0);
1359 }
1360 let share = |view: f32, whole: f32| (view / (whole * self.scale).max(f32::EPSILON)).min(1.0);
1361 let (w, h) = (share(self.viewport.width, content.width), share(self.viewport.height, content.height));
1362 Rect::new(self.center.x - w / 2.0, self.center.y - h / 2.0, w, h)
1363 }
1364
1365 fn fit_scale(&self, content: Size) -> f32 {
1367 let ratio = |view: f32, whole: f32| view / whole.max(f32::EPSILON);
1368 ratio(self.viewport.width, content.width).min(ratio(self.viewport.height, content.height))
1369 }
1370
1371 fn settled(mut self, viewport: Size, content: Size, range: ZoomRange) -> Self {
1374 self.viewport = viewport;
1375 let fit = self.fit_scale(content).max(f32::EPSILON);
1376 let (least, most) = range.limits(fit);
1377 self.scale = if self.fit { fit } else { self.scale.clamp(least, most) };
1378 let axis = |center: f32, view: f32, whole: f32| {
1379 let half = view / (2.0 * whole * self.scale).max(f32::EPSILON);
1380 if half >= 0.5 { 0.5 } else { center.clamp(half, 1.0 - half) }
1381 };
1382 self.center = Point::new(
1383 axis(self.center.x, viewport.width, content.width),
1384 axis(self.center.y, viewport.height, content.height),
1385 );
1386 self
1387 }
1388
1389 fn zoomed(self, scale: f32, anchor: Point, content: Size) -> Self {
1391 let before = self.placed(content);
1392 let fraction = |at: f32, start: f32, size: f32| (at - start) / size.max(f32::EPSILON);
1393 let (fx, fy) =
1394 (fraction(anchor.x, before.x(), before.width()), fraction(anchor.y, before.y(), before.height()));
1395 let (w, h) = (content.width * scale, content.height * scale);
1396 let center = Point::new(
1397 fx + (self.viewport.width / 2.0 - anchor.x) / w.max(f32::EPSILON),
1398 fy + (self.viewport.height / 2.0 - anchor.y) / h.max(f32::EPSILON),
1399 );
1400 Self { scale, center, fit: false, ..self }
1401 }
1402}
1403
1404#[derive(Clone, Copy, Debug)]
1406struct ZoomRange {
1407 least: Option<f32>,
1408 most: f32,
1409}
1410
1411#[cfg_attr(feature = "live", ravenrs_live::expose)]
1412impl ZoomRange {
1413 const DEFAULT: Self = Self { least: None, most: ZOOM_DEFAULT_MAX };
1414
1415 fn limits(self, fit: f32) -> (f32, f32) {
1417 (self.least.unwrap_or(fit).min(fit), self.most.max(fit))
1418 }
1419}
1420
1421fn zoom_percent(scale: f32, decimal: char) -> String {
1423 let percent = scale * 100.0;
1424 if percent < 100.0 && (percent - percent.round()).abs() >= 0.05 {
1425 format!("{percent:.1}%").replace('.', &decimal.to_string())
1426 } else {
1427 format!("{}%", percent.round())
1428 }
1429}
1430
1431#[derive(Debug)]
1447#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
1448pub struct ZoomControl {
1449 view: Binding<ZoomView>,
1450 least: f32,
1451 most: f32,
1452 size: ButtonSize,
1453 label: Option<String>,
1454}
1455
1456#[cfg_attr(feature = "live", ravenrs_live::expose)]
1457impl ZoomControl {
1458 pub fn new(view: Binding<ZoomView>) -> Self {
1460 Self {
1461 view,
1462 least: ZOOM_STOPS[0],
1463 most: ZOOM_STOPS[ZOOM_STOPS.len() - 1],
1464 size: ButtonSize::Small,
1465 label: None,
1466 }
1467 }
1468
1469 pub fn range(mut self, least: f32, most: f32) -> Self {
1472 self.least = least;
1473 self.most = most.max(least);
1474 self
1475 }
1476
1477 pub fn size(mut self, size: ButtonSize) -> Self {
1479 self.size = size;
1480 self
1481 }
1482
1483 pub fn label(mut self, label: impl Into<String>) -> Self {
1485 self.label = Some(label.into());
1486 self
1487 }
1488}
1489
1490impl View for ZoomControl {
1491 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
1492 let theme = *cx.theme();
1493 let labels = cx.labels();
1494 let current = self.view.get();
1495 let (least, most) = (self.least, self.most);
1496 let change = |f: fn(&mut ZoomView, f32, f32)| {
1497 let view = self.view.clone();
1498 move || view.update(|v| f(v, least, most))
1499 };
1500 let fit = Button::new(labels.zoom_fit.clone())
1501 .style(ButtonStyle::Ghost)
1502 .size(self.size)
1503 .selected(current.fit)
1504 .on_click(change(|v, _, _| v.fit = true));
1505 let actual = Button::new("1:1")
1506 .accessible_name(labels.zoom_actual.clone())
1507 .style(ButtonStyle::Ghost)
1508 .size(self.size)
1509 .selected(!current.fit && (current.scale - 1.0).abs() < 0.001)
1510 .on_click(change(|v, _, _| *v = ZoomView { scale: 1.0, fit: false, ..*v }));
1511 let out = IconButton::new(IconName::Minus)
1512 .label(labels.zoom_out.clone())
1513 .size(self.size)
1514 .disabled(current.scale <= least * 1.001)
1515 .on_click(change(|v, least, _| {
1516 *v = ZoomView { scale: zoom_stop(v.scale, false).max(least), fit: false, ..*v };
1517 }));
1518 let more = IconButton::new(IconName::Plus)
1519 .label(labels.zoom_in.clone())
1520 .size(self.size)
1521 .disabled(current.scale >= most * 0.999)
1522 .on_click(change(|v, _, most| {
1523 *v = ZoomView { scale: zoom_stop(v.scale, true).min(most), fit: false, ..*v };
1524 }));
1525 let nodes = [cx.child(&fit), cx.child(&actual)];
1526 let line = cx.container(
1527 LayoutStyle { width: Some(1.0), height: Some(16.0), ..LayoutStyle::leaf() },
1528 &[],
1529 Paint::Quad(QuadPaint::new(theme.color(Semantic::Border))),
1530 );
1531 let out = cx.child(&out);
1532 let percent = zoom_percent(current.scale, labels.decimal_separator);
1533 let attrs = util::attrs(&theme, TextStyle::Callout, None);
1534 let widest = cx.measure_text("1600%", &attrs).width.ceil();
1536 let text = util::label(cx, &percent, TextStyle::Callout, None, theme.color(Semantic::Ink));
1537 let number = cx.container(
1538 LayoutStyle {
1539 min_width: Some(widest),
1540 justify: Justify::Center,
1541 ..LayoutStyle::stack(Axis::Horizontal)
1542 },
1543 &[text],
1544 Paint::None,
1545 );
1546 let more = cx.child(&more);
1547 let node = cx.container(
1548 LayoutStyle {
1549 spacing: theme.spacing.s1,
1550 align: Align::Center,
1551 align_self: Some(Align::Fit),
1552 ..LayoutStyle::stack(Axis::Horizontal)
1553 },
1554 &[nodes[0], nodes[1], line, out, number, more],
1555 Paint::None,
1556 );
1557 let name = self.label.clone().unwrap_or_else(|| labels.zoom.clone());
1558 cx.semantics(node, Semantics::new(Role::Group).name(name));
1559 node
1560 }
1561}
1562
1563#[derive(Clone, Copy, Debug, PartialEq)]
1569#[non_exhaustive]
1570#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
1571pub struct ZoomPanPaint {
1572 pub frame: Rect,
1574 pub placed: Rect,
1577 pub scale: f32,
1579}
1580
1581#[derive(Clone)]
1584enum ZoomContent {
1585 Image(ImageData),
1586 Custom(ZoomDrawFn),
1587}
1588
1589type ZoomDrawFn = Rc<dyn Fn(ZoomPanPaint, &mut Scene)>;
1591
1592#[cfg_attr(feature = "live", ravenrs_live::expose)]
1593impl ZoomContent {
1594 fn paint(&self, scene: &mut Scene, frame: Rect, placed: Rect, scale: f32, radius: f32) {
1595 match self {
1596 Self::Image(image) => {
1597 let shown = placed.intersect(&frame);
1600 if shown.is_empty() || placed.is_empty() {
1601 return;
1602 }
1603 let mut quad = ImageQuad::new(shown, image.clone()).radius(radius);
1604 quad.uv = [
1605 (shown.x() - placed.x()) / placed.width(),
1606 (shown.y() - placed.y()) / placed.height(),
1607 (shown.max_x() - placed.x()) / placed.width(),
1608 (shown.max_y() - placed.y()) / placed.height(),
1609 ];
1610 scene.push_image(quad);
1611 }
1612 Self::Custom(draw) => {
1613 scene.push_clip(frame);
1614 draw(ZoomPanPaint { frame, placed, scale }, scene);
1615 scene.pop_clip();
1616 }
1617 }
1618 }
1619}
1620
1621#[derive(Clone, Copy, Debug)]
1624struct Glide {
1625 started: f64,
1626 from: Point,
1628 target: Point,
1630 velocity: Point,
1631 written: ZoomView,
1633}
1634
1635const GLIDE_RESPONSE: f32 = 1.4;
1637const GLIDE_MIN_SPEED: f32 = 90.0;
1639const GLIDE_REST: (f32, f32) = (0.5, 8.0);
1641
1642#[derive(Debug, Default)]
1644struct ZoomPanMemory {
1645 frame: Cell<Rect>,
1646 pointer: Cell<Option<Point>>,
1647 grab: Cell<Option<(Point, Point)>>,
1649 clock: Cell<f64>,
1650 sample: Cell<Option<(f64, Point)>>,
1652 velocity: Cell<Point>,
1653 glide: Cell<Option<Glide>>,
1654 scroll: Cell<Point>,
1656 focused: Cell<bool>,
1658}
1659
1660#[derive(Clone)]
1662struct ZoomPanWiring {
1663 view: Binding<ZoomView>,
1664 content: Size,
1665 range: ZoomRange,
1666 memory: Rc<ZoomPanMemory>,
1667 live: State<bool>,
1668}
1669
1670#[cfg_attr(feature = "live", ravenrs_live::expose)]
1671impl ZoomPanWiring {
1672 fn current(&self, viewport: Size) -> ZoomView {
1673 self.view.get().settled(viewport, self.content, self.range)
1674 }
1675
1676 fn write(&self, next: ZoomView) {
1677 if self.view.get() != next {
1678 self.view.set(next);
1679 }
1680 }
1681
1682 fn zoom_to(&self, scale: f32, anchor: Point) {
1685 self.memory.glide.set(None);
1686 let viewport = self.memory.frame.get().size;
1687 let current = self.current(viewport);
1688 let mut next =
1689 current.zoomed(scale, anchor, self.content).settled(viewport, self.content, self.range);
1690 let fit = next.fit_scale(self.content);
1691 next.fit = (next.scale - fit).abs() <= fit * 0.0005;
1692 self.write(next);
1693 }
1694
1695 fn pan_by(&self, dx: f32, dy: f32) {
1696 self.memory.glide.set(None);
1697 let viewport = self.memory.frame.get().size;
1698 let v = self.current(viewport);
1699 let (w, h) = self.scaled(v.scale);
1700 let center = Point::new(v.center.x + dx / w, v.center.y + dy / h);
1701 self.write(ZoomView { center, ..v }.settled(viewport, self.content, self.range));
1702 }
1703
1704 fn scaled(&self, scale: f32) -> (f32, f32) {
1706 ((self.content.width * scale).max(f32::EPSILON), (self.content.height * scale).max(f32::EPSILON))
1707 }
1708
1709 fn on_drag(&self, d: Drag) {
1710 let memory = &self.memory;
1711 memory.frame.set(d.frame);
1712 match d.phase {
1713 DragPhase::Start => {
1714 memory.glide.set(None);
1715 memory.grab.set(Some((d.position, self.current(d.frame.size).center)));
1716 memory.sample.set(None);
1717 memory.velocity.set(Point::ZERO);
1718 self.live.set_if_changed(true);
1719 }
1720 DragPhase::Move => {
1721 let Some((pressed, center)) = memory.grab.get() else { return };
1722 let v = self.current(d.frame.size);
1723 let (w, h) = self.scaled(v.scale);
1724 let center = Point::new(
1725 center.x - (d.position.x - pressed.x) / w,
1726 center.y - (d.position.y - pressed.y) / h,
1727 );
1728 self.write(ZoomView { center, ..v }.settled(d.frame.size, self.content, self.range));
1729 }
1730 DragPhase::End => {
1731 memory.grab.set(None);
1732 self.release(d.frame.size);
1733 }
1734 }
1735 }
1736
1737 fn release(&self, viewport: Size) {
1739 let memory = &self.memory;
1740 let velocity = memory.velocity.get();
1741 if velocity.x.hypot(velocity.y) < GLIDE_MIN_SPEED {
1742 self.live.set_if_changed(false);
1743 return;
1744 }
1745 let v = self.current(viewport);
1746 let (w, h) = self.scaled(v.scale);
1747 let at = Point::new(v.center.x * w, v.center.y * h);
1748 let spring = glide_spring();
1750 let reach = spring.stiffness.sqrt();
1751 let thrown = Point::new((at.x + velocity.x / reach) / w, (at.y + velocity.y / reach) / h);
1752 let end = ZoomView { center: thrown, ..v }.settled(viewport, self.content, self.range);
1753 let target = Point::new(end.center.x * w, end.center.y * h);
1754 memory.glide.set(Some(Glide {
1755 started: memory.clock.get(),
1756 from: Point::new(at.x - target.x, at.y - target.y),
1757 target,
1758 velocity,
1759 written: v,
1760 }));
1761 }
1762
1763 fn on_frame(&self, viewport: Size, time: f64) {
1766 let memory = &self.memory;
1767 memory.clock.set(time);
1768 if memory.grab.get().is_some() {
1769 let v = self.current(viewport);
1770 let (w, h) = self.scaled(v.scale);
1771 let at = Point::new(v.center.x * w, v.center.y * h);
1772 if let Some((then, was)) = memory.sample.get() {
1773 let dt = (time - then) as f32;
1774 if dt > 0.0 {
1775 let before = memory.velocity.get();
1776 memory.velocity.set(Point::new(
1777 before.x * 0.4 + (at.x - was.x) / dt * 0.6,
1778 before.y * 0.4 + (at.y - was.y) / dt * 0.6,
1779 ));
1780 memory.sample.set(Some((time, at)));
1781 }
1782 } else {
1783 memory.sample.set(Some((time, at)));
1784 }
1785 } else if let Some(mut glide) = memory.glide.get() {
1786 let v = self.current(viewport);
1787 if v.center != glide.written.center || v.scale != glide.written.scale {
1788 memory.glide.set(None);
1789 self.live.set_if_changed(false);
1790 return;
1791 }
1792 let (spring, t) = (glide_spring(), (time - glide.started) as f32);
1793 let (x, vx) = spring.sample(glide.from.x, glide.velocity.x, t);
1794 let (y, vy) = spring.sample(glide.from.y, glide.velocity.y, t);
1795 let (w, h) = self.scaled(v.scale);
1796 let center = Point::new((glide.target.x + x) / w, (glide.target.y + y) / h);
1797 let next = ZoomView { center, ..v }.settled(viewport, self.content, self.range);
1798 glide.written = next;
1799 self.write(next);
1800 if x.hypot(y) < GLIDE_REST.0 && vx.hypot(vy) < GLIDE_REST.1 {
1801 memory.glide.set(None);
1802 self.live.set_if_changed(false);
1803 } else {
1804 memory.glide.set(Some(glide));
1805 }
1806 }
1807 }
1808
1809 fn on_key(&self, key: &Key, shift: bool) -> bool {
1810 let viewport = self.memory.frame.get().size;
1811 let middle = Point::new(viewport.width / 2.0, viewport.height / 2.0);
1812 let v = self.current(viewport);
1813 let step = if shift { 0.5 } else { 0.1 };
1814 match key {
1815 Key::Character(c) => match c.as_str() {
1816 "+" | "=" => self.zoom_to(zoom_stop(v.scale, true), middle),
1817 "-" | "_" => self.zoom_to(zoom_stop(v.scale, false), middle),
1818 "0" => {
1819 self.memory.glide.set(None);
1820 self.write(ZoomView { fit: true, ..v }.settled(viewport, self.content, self.range));
1821 }
1822 "1" => self.zoom_to(1.0, middle),
1823 _ => return false,
1824 },
1825 Key::ArrowLeft => self.pan_by(-viewport.width * step, 0.0),
1826 Key::ArrowRight => self.pan_by(viewport.width * step, 0.0),
1827 Key::ArrowUp => self.pan_by(0.0, -viewport.height * step),
1828 Key::ArrowDown => self.pan_by(0.0, viewport.height * step),
1829 _ => return false,
1830 }
1831 true
1832 }
1833
1834 fn on_double_click(&self) {
1835 let viewport = self.memory.frame.get().size;
1836 let v = self.current(viewport);
1837 if v.fit {
1838 let target = if v.scale < 0.99 { 1.0 } else { v.scale * 2.0 };
1840 let middle = Point::new(viewport.width / 2.0, viewport.height / 2.0);
1841 let at = self.memory.pointer.get().unwrap_or(middle);
1842 self.zoom_to(target, at);
1843 } else {
1844 self.memory.glide.set(None);
1845 self.write(ZoomView { fit: true, ..v }.settled(viewport, self.content, self.range));
1846 }
1847 }
1848}
1849
1850fn glide_spring() -> Spring {
1851 Spring::from_response(GLIDE_RESPONSE, 1.0)
1852}
1853
1854#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
1880pub struct ZoomPanView {
1881 view: Binding<ZoomView>,
1882 content: Size,
1883 picture: ZoomContent,
1884 width: Option<f32>,
1885 height: Option<f32>,
1886 grow: f32,
1887 range: ZoomRange,
1888 inertia: bool,
1889 label: Option<String>,
1890 defer_arrows: bool,
1892 autofocus: bool,
1894}
1895
1896impl fmt::Debug for ZoomPanView {
1897 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1898 f.debug_struct("ZoomPanView").field("content", &self.content).finish_non_exhaustive()
1899 }
1900}
1901
1902#[cfg_attr(feature = "live", ravenrs_live::expose)]
1903impl ZoomPanView {
1904 pub fn image(view: Binding<ZoomView>, image: ImageData) -> Self {
1906 let content = Size::new(image.width() as f32, image.height() as f32);
1907 Self::with(view, content, ZoomContent::Image(image))
1908 }
1909
1910 pub fn custom(
1912 view: Binding<ZoomView>,
1913 content: Size,
1914 draw: impl Fn(ZoomPanPaint, &mut Scene) + 'static,
1915 ) -> Self {
1916 Self::with(view, content, ZoomContent::Custom(Rc::new(draw)))
1917 }
1918
1919 fn with(view: Binding<ZoomView>, content: Size, picture: ZoomContent) -> Self {
1920 Self {
1921 view,
1922 content,
1923 picture,
1924 width: None,
1925 height: None,
1926 grow: 1.0,
1927 range: ZoomRange::DEFAULT,
1928 inertia: true,
1929 label: None,
1930 defer_arrows: false,
1931 autofocus: false,
1932 }
1933 }
1934
1935 pub fn size(mut self, width: f32, height: f32) -> Self {
1937 self.width = Some(width);
1938 self.height = Some(height);
1939 self.grow = 0.0;
1940 self
1941 }
1942
1943 pub fn grow(mut self, grow: f32) -> Self {
1945 self.grow = grow;
1946 self
1947 }
1948
1949 pub fn range(mut self, least: f32, most: f32) -> Self {
1952 self.range = ZoomRange { least: Some(least), most };
1953 self
1954 }
1955
1956 pub fn inertia(mut self, inertia: bool) -> Self {
1958 self.inertia = inertia;
1959 self
1960 }
1961
1962 pub fn label(mut self, label: impl Into<String>) -> Self {
1964 self.label = Some(label.into());
1965 self
1966 }
1967}
1968
1969impl View for ZoomPanView {
1970 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
1971 let labels = cx.labels();
1972 let radius = cx.theme().radii.sm;
1973 let live = cx.use_state(|| false);
1974 let is_live = live.get();
1975 let stored = if is_live { Observer::untracked(|| self.view.get()) } else { self.view.get() };
1978 let memory = cx.use_state(|| Rc::new(ZoomPanMemory::default())).get();
1979 let (content, range) = (self.content, self.range);
1980 let view = match cx.scroll_viewport() {
1981 Some(viewport) => {
1982 let mut view = stored.settled(viewport, content, range);
1983 let offset = cx.scroll_offset();
1985 let asked = memory.scroll.get();
1986 if (offset.x - asked.x).abs() > 0.01 || (offset.y - asked.y).abs() > 0.01 {
1987 let (w, h) = (content.width * view.scale, content.height * view.scale);
1988 view.center = Point::new(
1989 (offset.x + viewport.width / 2.0) / w.max(f32::EPSILON),
1990 (offset.y + viewport.height / 2.0) / h.max(f32::EPSILON),
1991 );
1992 view = view.settled(viewport, content, range);
1993 }
1994 let placed = view.placed(content);
1995 let wanted = Point::new((-placed.x()).max(0.0), (-placed.y()).max(0.0));
1996 if (wanted.x - offset.x).abs() > 0.01 || (wanted.y - offset.y).abs() > 0.01 {
1997 cx.set_scroll_offset(wanted);
1998 memory.scroll.set(wanted);
1999 } else {
2000 memory.scroll.set(offset);
2001 }
2002 if view != stored {
2003 self.view.set(view);
2004 }
2005 view
2006 }
2007 None => stored,
2008 };
2009 let wiring = ZoomPanWiring {
2010 view: self.view.clone(),
2011 content,
2012 range,
2013 memory: memory.clone(),
2014 live: live.clone(),
2015 };
2016 let room = cx.container(
2018 LayoutStyle {
2019 width: Some(content.width * view.scale),
2020 height: Some(content.height * view.scale),
2021 ..LayoutStyle::leaf()
2022 },
2023 &[],
2024 Paint::None,
2025 );
2026 let picture = self.picture.clone();
2027 let draw = {
2028 let wiring = wiring.clone();
2029 move |frame: Rect, scene: &mut Scene| {
2030 wiring.memory.frame.set(frame);
2031 let v = wiring.current(frame.size);
2032 let placed = v.placed(content);
2033 let on_screen = Rect::new(
2034 frame.x() + placed.x(),
2035 frame.y() + placed.y(),
2036 placed.width(),
2037 placed.height(),
2038 );
2039 picture.paint(scene, frame, on_screen, v.scale, 0.0);
2040 }
2041 };
2042 let paint = if is_live {
2043 let wiring = wiring.clone();
2044 Paint::Live(Rc::new(move |frame: Rect, time, scene: &mut Scene| {
2045 wiring.on_frame(frame.size, time);
2046 draw(frame, scene);
2047 }))
2048 } else {
2049 Paint::Custom(Rc::new(draw))
2050 };
2051 let node = cx.container(
2052 LayoutStyle {
2053 width: self.width,
2054 height: self.height,
2055 grow: self.grow,
2056 align: Align::Start,
2057 align_self: Some(if self.width.is_some() { Align::Start } else { Align::Stretch }),
2058 overflow_hidden: true,
2059 ..LayoutStyle::stack(Axis::Vertical)
2060 },
2061 &[room],
2062 paint,
2063 );
2064 cx.scrollable_virtual(node, true);
2065 let name = self.label.clone().unwrap_or_else(|| labels.zoom_view.clone());
2066 cx.semantics(
2067 node,
2068 Semantics::new(Role::Image).name(name).value(zoom_percent(view.scale, labels.decimal_separator)),
2069 );
2070 cx.focusable(node, radius);
2071 let pannable = {
2072 let placed = view.placed(content);
2073 placed.width() > view.viewport.width + 0.5 || placed.height() > view.viewport.height + 0.5
2074 };
2075 if pannable {
2076 cx.cursor(node, CursorIcon::Pointer);
2077 }
2078 let inertia = self.inertia && !cx.reduced_motion();
2079 let dragged = wiring.clone();
2080 cx.on_drag(
2081 node,
2082 Rc::new(move |d| {
2083 if !inertia {
2084 dragged.memory.velocity.set(Point::ZERO);
2085 }
2086 dragged.on_drag(d);
2087 }),
2088 );
2089 let moved = wiring.memory.clone();
2090 cx.on_pointer_move(
2091 node,
2092 Rc::new(move |at, frame| {
2093 moved.pointer.set(Some(Point::new(at.x - frame.x(), at.y - frame.y())));
2094 }),
2095 );
2096 let pinched = wiring.clone();
2097 cx.on_pinch(
2098 node,
2099 Rc::new(move |delta| {
2100 let viewport = pinched.memory.frame.get().size;
2101 let middle = Point::new(viewport.width / 2.0, viewport.height / 2.0);
2102 let at = pinched.memory.pointer.get().unwrap_or(middle);
2103 let scale = pinched.current(viewport).scale * (1.0 + delta).max(0.05);
2104 pinched.zoom_to(scale, at);
2105 }),
2106 );
2107 let doubled = wiring.clone();
2108 cx.on_double_click(node, Rc::new(move || doubled.on_double_click()));
2109 let defer_arrows = self.defer_arrows;
2110 cx.on_key(
2111 node,
2112 Rc::new(move |key, modifiers| {
2113 let arrow = matches!(key, Key::ArrowLeft | Key::ArrowRight | Key::ArrowUp | Key::ArrowDown);
2114 if defer_arrows && arrow && wiring.current(wiring.memory.frame.get().size).fit {
2115 return false;
2116 }
2117 wiring.on_key(key, modifiers.shift)
2118 }),
2119 );
2120 if self.autofocus && !memory.focused.replace(true) {
2121 cx.request_focus();
2122 }
2123 node
2124 }
2125}
2126
2127#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
2146pub struct Minimap {
2147 view: Binding<ZoomView>,
2148 content: Size,
2149 picture: ZoomContent,
2150 width: f32,
2151 max_height: f32,
2152 press_zoom: Option<f32>,
2153 label: Option<String>,
2154}
2155
2156impl fmt::Debug for Minimap {
2157 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2158 f.debug_struct("Minimap").field("content", &self.content).finish_non_exhaustive()
2159 }
2160}
2161
2162#[cfg_attr(feature = "live", ravenrs_live::expose)]
2163impl Minimap {
2164 pub fn image(view: Binding<ZoomView>, image: ImageData) -> Self {
2166 let content = Size::new(image.width() as f32, image.height() as f32);
2167 Self::with(view, content, ZoomContent::Image(image))
2168 }
2169
2170 pub fn custom(
2172 view: Binding<ZoomView>,
2173 content: Size,
2174 draw: impl Fn(ZoomPanPaint, &mut Scene) + 'static,
2175 ) -> Self {
2176 Self::with(view, content, ZoomContent::Custom(Rc::new(draw)))
2177 }
2178
2179 fn with(view: Binding<ZoomView>, content: Size, picture: ZoomContent) -> Self {
2180 Self { view, content, picture, width: 160.0, max_height: 160.0, press_zoom: None, label: None }
2181 }
2182
2183 pub fn content_size(mut self, content: Size) -> Self {
2185 self.content = content;
2186 self
2187 }
2188
2189 pub fn max_size(mut self, width: f32, height: f32) -> Self {
2192 self.width = width;
2193 self.max_height = height;
2194 self
2195 }
2196
2197 pub fn press_zoom(mut self, scale: f32) -> Self {
2200 self.press_zoom = Some(scale);
2201 self
2202 }
2203
2204 pub fn label(mut self, label: impl Into<String>) -> Self {
2206 self.label = Some(label.into());
2207 self
2208 }
2209}
2210
2211fn minimap_center(view: ZoomView, content: Size, center: Point) -> ZoomView {
2213 let visible = view.visible(content);
2214 let axis = |value: f32, size: f32| {
2215 let half = size / 2.0;
2216 if half >= 0.5 { 0.5 } else { value.clamp(half, 1.0 - half) }
2217 };
2218 let center = Point::new(axis(center.x, visible.width()), axis(center.y, visible.height()));
2219 ZoomView { center, ..view }
2220}
2221
2222impl View for Minimap {
2223 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
2224 let theme = *cx.theme();
2225 let labels = cx.labels();
2226 let current = self.view.get();
2227 let content = self.content;
2228 let aspect = content.width / content.height.max(f32::EPSILON);
2229 let (width, height) = if self.width / aspect <= self.max_height {
2230 (self.width, self.width / aspect)
2231 } else {
2232 (self.max_height * aspect, self.max_height)
2233 };
2234 let (background, border, scrim, accent) = (
2235 theme.color(Semantic::Canvas),
2236 theme.color(Semantic::Border),
2237 theme.color(Semantic::Scrim),
2238 theme.color(Semantic::Accent),
2239 );
2240 let radius = theme.radii.sm;
2241 let picture = self.picture.clone();
2242 let visible = current.visible(content);
2243 let node = cx.container(
2244 LayoutStyle {
2245 width: Some(width),
2246 height: Some(height),
2247 align_self: Some(Align::Start),
2248 ..LayoutStyle::leaf()
2249 },
2250 &[],
2251 Paint::Custom(Rc::new(move |frame: Rect, scene| {
2252 scene.push_quad(Quad::new(frame, background).radius(radius));
2253 picture.paint(scene, frame, frame, frame.width() / content.width.max(f32::EPSILON), radius);
2254 scene.push_quad(Quad::new(frame, Color::TRANSPARENT).radius(radius).border(1.0, border));
2255 if visible.width() >= 0.999 && visible.height() >= 0.999 {
2256 return;
2257 }
2258 let shown = Rect::new(
2259 frame.x() + visible.x() * frame.width(),
2260 frame.y() + visible.y() * frame.height(),
2261 visible.width() * frame.width(),
2262 visible.height() * frame.height(),
2263 );
2264 let (above, below) = (shown.y() - frame.y(), frame.max_y() - shown.max_y());
2266 let (left, right) = (shown.x() - frame.x(), frame.max_x() - shown.max_x());
2267 for part in [
2268 Rect::new(frame.x(), frame.y(), frame.width(), above),
2269 Rect::new(frame.x(), shown.max_y(), frame.width(), below),
2270 Rect::new(frame.x(), shown.y(), left, shown.height()),
2271 Rect::new(shown.max_x(), shown.y(), right, shown.height()),
2272 ] {
2273 if !part.is_empty() {
2274 scene.push_quad(Quad::new(part, scrim));
2275 }
2276 }
2277 scene.push_quad(Quad::new(shown, Color::TRANSPARENT).border(1.5, accent));
2278 })),
2279 );
2280 let name = self.label.clone().unwrap_or_else(|| labels.minimap.clone());
2281 let percent = |v: f32| (v * 100.0).round();
2282 let text = format!(
2283 "{} · {}%, {}%",
2284 zoom_percent(current.scale, labels.decimal_separator),
2285 percent(current.center.x),
2286 percent(current.center.y)
2287 );
2288 cx.semantics(node, Semantics::new(Role::Slider).name(name).value(text));
2289 cx.focusable(node, theme.radii.sm);
2290 cx.cursor(node, CursorIcon::Pointer);
2291 let (dragged, press_zoom) = (self.view.clone(), self.press_zoom);
2292 cx.on_drag(
2293 node,
2294 Rc::new(move |d| {
2295 if d.phase == DragPhase::End {
2296 return;
2297 }
2298 let center = Point::new(
2299 ((d.position.x - d.frame.x()) / d.frame.width().max(1.0)).clamp(0.0, 1.0),
2300 ((d.position.y - d.frame.y()) / d.frame.height().max(1.0)).clamp(0.0, 1.0),
2301 );
2302 let mut view = dragged.get();
2303 let everything = view.visible(content);
2304 if everything.width() >= 0.999 && everything.height() >= 0.999 {
2305 let Some(scale) = press_zoom else { return };
2306 view = ZoomView { scale, fit: false, ..view };
2307 }
2308 let next = minimap_center(view, content, center);
2309 if next != dragged.get() {
2310 dragged.set(next);
2311 }
2312 }),
2313 );
2314 let keyed = self.view.clone();
2315 cx.on_key(
2316 node,
2317 Rc::new(move |key, modifiers| {
2318 let step = if modifiers.shift { 0.25 } else { 0.05 };
2319 let (dx, dy) = match key {
2320 Key::ArrowLeft => (-step, 0.0),
2321 Key::ArrowRight => (step, 0.0),
2322 Key::ArrowUp => (0.0, -step),
2323 Key::ArrowDown => (0.0, step),
2324 _ => return false,
2325 };
2326 let view = keyed.get();
2327 let center = Point::new(view.center.x + dx, view.center.y + dy);
2328 keyed.set(minimap_center(view, content, center));
2329 true
2330 }),
2331 );
2332 node
2333 }
2334}