1use llimphi_ui::llimphi_layout::taffy::{
18 prelude::{length, percent, Dimension, FlexDirection, Size, Style},
19 AlignItems, JustifyContent,
20};
21use llimphi_ui::llimphi_raster::kurbo::{Affine, Circle, Line, Stroke};
22use llimphi_ui::llimphi_raster::peniko::{Color, Fill};
23use llimphi_ui::{App, DragPhase, GesturePhase, Handle, View};
24
25#[derive(Clone)]
26enum Msg {
27 Zoom { factor: f32, fx: f32, fy: f32 },
29 Pan { dx: f32, dy: f32 },
31 Reset,
33 Mark { lx: f32, ly: f32 },
35}
36
37struct Model {
38 zoom: f32,
39 pan: (f32, f32),
40 marks: Vec<(f32, f32)>,
42 last: String,
43}
44
45struct Gestos;
46
47impl App for Gestos {
48 type Model = Model;
49 type Msg = Msg;
50
51 fn title() -> &'static str {
52 "llimphi · gestos (pinch-zoom · double-tap · long-press)"
53 }
54
55 fn initial_size() -> (u32, u32) {
56 (900, 640)
57 }
58
59 fn init(_: &Handle<Self::Msg>) -> Self::Model {
60 Model {
61 zoom: 1.0,
62 pan: (0.0, 0.0),
63 marks: Vec::new(),
64 last: "probá: Ctrl+rueda (zoom) · arrastrar (paneo) · doble-click (reset) · mantener (marca)".into(),
65 }
66 }
67
68 fn update(mut model: Self::Model, msg: Self::Msg, _: &Handle<Self::Msg>) -> Self::Model {
69 match msg {
70 Msg::Zoom { factor, fx, fy } => {
71 let new_zoom = (model.zoom * factor).clamp(0.15, 12.0);
74 let rf = new_zoom / model.zoom; model.pan.0 = fx - rf * (fx - model.pan.0);
76 model.pan.1 = fy - rf * (fy - model.pan.1);
77 model.zoom = new_zoom;
78 model.last = format!("zoom ×{:.2}", model.zoom);
79 }
80 Msg::Pan { dx, dy } => {
81 model.pan.0 += dx;
82 model.pan.1 += dy;
83 model.last = "paneo".into();
84 }
85 Msg::Reset => {
86 model.zoom = 1.0;
87 model.pan = (0.0, 0.0);
88 model.last = "doble-tap → reset".into();
89 }
90 Msg::Mark { lx, ly } => {
91 let wx = (lx - model.pan.0) / model.zoom;
93 let wy = (ly - model.pan.1) / model.zoom;
94 model.marks.push((wx, wy));
95 model.last = format!("long-press → marca #{} @ ({wx:.0}, {wy:.0})", model.marks.len());
96 }
97 }
98 model
99 }
100
101 fn view(model: &Self::Model) -> View<Self::Msg> {
102 let zoom = model.zoom;
103 let pan = model.pan;
104 let marks = model.marks.clone();
105
106 let canvas = View::new(Style {
107 size: Size { width: percent(1.0_f32), height: Dimension::auto() },
108 flex_grow: 1.0,
109 ..Default::default()
110 })
111 .fill(Color::from_rgba8(16, 18, 26, 255))
112 .clip(true)
113 .paint_with(move |scene, _ts, rect| {
114 let step = 40.0 * zoom as f64;
116 if step >= 4.0 {
117 let thin = Stroke::new(1.0);
118 let grid = Color::from_rgba8(40, 46, 60, 255);
119 let ox = (rect.x as f64) + (pan.0 as f64).rem_euclid(step);
121 let mut x = ox;
122 while x < (rect.x + rect.w) as f64 {
123 scene.stroke(&thin, Affine::IDENTITY, grid, None,
124 &Line::new((x, rect.y as f64), (x, (rect.y + rect.h) as f64)));
125 x += step;
126 }
127 let oy = (rect.y as f64) + (pan.1 as f64).rem_euclid(step);
128 let mut y = oy;
129 while y < (rect.y + rect.h) as f64 {
130 scene.stroke(&thin, Affine::IDENTITY, grid, None,
131 &Line::new((rect.x as f64, y), ((rect.x + rect.w) as f64, y)));
132 y += step;
133 }
134 }
135 let dot = Color::from_rgba8(90, 220, 150, 255);
137 let r = (6.0 * zoom as f64).clamp(3.0, 24.0);
138 for (wx, wy) in &marks {
139 let sx = rect.x as f64 + pan.0 as f64 + (*wx as f64) * zoom as f64;
140 let sy = rect.y as f64 + pan.1 as f64 + (*wy as f64) * zoom as f64;
141 scene.fill(Fill::NonZero, Affine::IDENTITY, dot, None, &Circle::new((sx, sy), r));
142 }
143 })
144 .on_scale(|phase, factor, fx, fy| match phase {
146 GesturePhase::Update => Some(Msg::Zoom { factor, fx, fy }),
147 _ => None,
148 })
149 .draggable(|phase, dx, dy| match phase {
151 DragPhase::Move => Some(Msg::Pan { dx, dy }),
152 DragPhase::End => None,
153 })
154 .on_double_tap(Msg::Reset)
156 .on_long_press_at(|lx, ly, _w, _h| Some(Msg::Mark { lx, ly }));
157
158 let status = View::new(Style {
159 size: Size { width: percent(1.0_f32), height: length(40.0_f32) },
160 align_items: Some(AlignItems::Center),
161 justify_content: Some(JustifyContent::Center),
162 ..Default::default()
163 })
164 .fill(Color::from_rgba8(28, 32, 42, 255))
165 .text(
166 format!("{} · ×{:.2} · {} marcas", model.last, model.zoom, model.marks.len()),
167 18.0,
168 Color::from_rgba8(210, 220, 235, 255),
169 );
170
171 View::new(Style {
172 flex_direction: FlexDirection::Column,
173 size: Size { width: percent(1.0_f32), height: percent(1.0_f32) },
174 ..Default::default()
175 })
176 .fill(Color::from_rgba8(16, 18, 26, 255))
177 .children(vec![canvas, status])
178 }
179}
180
181fn main() {
182 llimphi_ui::run::<Gestos>();
183}