1use crate::core::border;
3use crate::core::image::{self, FilterMethod};
4use crate::core::layout;
5use crate::core::mouse;
6use crate::core::renderer;
7use crate::core::widget::tree::{self, Tree};
8use crate::core::{
9 Clipboard, ContentFit, Element, Event, Image, Layout, Length, Pixels,
10 Point, Radians, Rectangle, Shell, Size, Vector, Widget,
11};
12
13pub struct Viewer<Handle> {
15 padding: f32,
16 width: Length,
17 height: Length,
18 min_scale: f32,
19 max_scale: f32,
20 scale_step: f32,
21 handle: Handle,
22 filter_method: FilterMethod,
23 content_fit: ContentFit,
24}
25
26impl<Handle> Viewer<Handle> {
27 pub fn new<T: Into<Handle>>(handle: T) -> Self {
29 Viewer {
30 handle: handle.into(),
31 padding: 0.0,
32 width: Length::Shrink,
33 height: Length::Shrink,
34 min_scale: 0.25,
35 max_scale: 10.0,
36 scale_step: 0.10,
37 filter_method: FilterMethod::default(),
38 content_fit: ContentFit::default(),
39 }
40 }
41
42 pub fn filter_method(mut self, filter_method: image::FilterMethod) -> Self {
44 self.filter_method = filter_method;
45 self
46 }
47
48 pub fn content_fit(mut self, content_fit: ContentFit) -> Self {
50 self.content_fit = content_fit;
51 self
52 }
53
54 pub fn padding(mut self, padding: impl Into<Pixels>) -> Self {
56 self.padding = padding.into().0;
57 self
58 }
59
60 pub fn width(mut self, width: impl Into<Length>) -> Self {
62 self.width = width.into();
63 self
64 }
65
66 pub fn height(mut self, height: impl Into<Length>) -> Self {
68 self.height = height.into();
69 self
70 }
71
72 pub fn max_scale(mut self, max_scale: f32) -> Self {
76 self.max_scale = max_scale;
77 self
78 }
79
80 pub fn min_scale(mut self, min_scale: f32) -> Self {
84 self.min_scale = min_scale;
85 self
86 }
87
88 pub fn scale_step(mut self, scale_step: f32) -> Self {
93 self.scale_step = scale_step;
94 self
95 }
96}
97
98impl<Message, Theme, Renderer, Handle> Widget<Message, Theme, Renderer>
99 for Viewer<Handle>
100where
101 Renderer: image::Renderer<Handle = Handle>,
102 Handle: Clone,
103{
104 fn tag(&self) -> tree::Tag {
105 tree::Tag::of::<State>()
106 }
107
108 fn state(&self) -> tree::State {
109 tree::State::new(State::new())
110 }
111
112 fn size(&self) -> Size<Length> {
113 Size {
114 width: self.width,
115 height: self.height,
116 }
117 }
118
119 fn layout(
120 &mut self,
121 _tree: &mut Tree,
122 renderer: &Renderer,
123 limits: &layout::Limits,
124 ) -> layout::Node {
125 let image_size =
127 renderer.measure_image(&self.handle).unwrap_or_default();
128
129 let image_size =
130 Size::new(image_size.width as f32, image_size.height as f32);
131
132 let raw_size = limits.resolve(self.width, self.height, image_size);
134
135 let full_size = self.content_fit.fit(image_size, raw_size);
137
138 let final_size = Size {
140 width: match self.width {
141 Length::Shrink => f32::min(raw_size.width, full_size.width),
142 _ => raw_size.width,
143 },
144 height: match self.height {
145 Length::Shrink => f32::min(raw_size.height, full_size.height),
146 _ => raw_size.height,
147 },
148 };
149
150 layout::Node::new(final_size)
151 }
152
153 fn update(
154 &mut self,
155 tree: &mut Tree,
156 event: &Event,
157 layout: Layout<'_>,
158 cursor: mouse::Cursor,
159 renderer: &Renderer,
160 _clipboard: &mut dyn Clipboard,
161 shell: &mut Shell<'_, Message>,
162 _viewport: &Rectangle,
163 ) {
164 let bounds = layout.bounds();
165
166 match event {
167 Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
168 let Some(cursor_position) = cursor.position_over(bounds) else {
169 return;
170 };
171
172 match *delta {
173 mouse::ScrollDelta::Lines { y, .. }
174 | mouse::ScrollDelta::Pixels { y, .. } => {
175 let state = tree.state.downcast_mut::<State>();
176 let previous_scale = state.scale;
177
178 if y < 0.0 && previous_scale > self.min_scale
179 || y > 0.0 && previous_scale < self.max_scale
180 {
181 state.scale = (if y > 0.0 {
182 state.scale * (1.0 + self.scale_step)
183 } else {
184 state.scale / (1.0 + self.scale_step)
185 })
186 .clamp(self.min_scale, self.max_scale);
187
188 let scaled_size = scaled_image_size(
189 renderer,
190 &self.handle,
191 state,
192 bounds.size(),
193 self.content_fit,
194 );
195
196 let factor = state.scale / previous_scale - 1.0;
197
198 let cursor_to_center =
199 cursor_position - bounds.center();
200
201 let adjustment = cursor_to_center * factor
202 + state.current_offset * factor;
203
204 state.current_offset = Vector::new(
205 if scaled_size.width > bounds.width {
206 state.current_offset.x + adjustment.x
207 } else {
208 0.0
209 },
210 if scaled_size.height > bounds.height {
211 state.current_offset.y + adjustment.y
212 } else {
213 0.0
214 },
215 );
216 }
217 }
218 }
219
220 shell.request_redraw();
221 shell.capture_event();
222 }
223 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
224 let Some(cursor_position) = cursor.position_over(bounds) else {
225 return;
226 };
227
228 let state = tree.state.downcast_mut::<State>();
229
230 state.cursor_grabbed_at = Some(cursor_position);
231 state.starting_offset = state.current_offset;
232
233 shell.request_redraw();
234 shell.capture_event();
235 }
236 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
237 let state = tree.state.downcast_mut::<State>();
238
239 if state.cursor_grabbed_at.is_some() {
240 state.cursor_grabbed_at = None;
241 shell.request_redraw();
242 shell.capture_event();
243 }
244 }
245 Event::Mouse(mouse::Event::CursorMoved { position }) => {
246 let state = tree.state.downcast_mut::<State>();
247
248 if let Some(origin) = state.cursor_grabbed_at {
249 let scaled_size = scaled_image_size(
250 renderer,
251 &self.handle,
252 state,
253 bounds.size(),
254 self.content_fit,
255 );
256 let hidden_width = (scaled_size.width - bounds.width / 2.0)
257 .max(0.0)
258 .round();
259
260 let hidden_height = (scaled_size.height
261 - bounds.height / 2.0)
262 .max(0.0)
263 .round();
264
265 let delta = *position - origin;
266
267 let x = if bounds.width < scaled_size.width {
268 (state.starting_offset.x - delta.x)
269 .clamp(-hidden_width, hidden_width)
270 } else {
271 0.0
272 };
273
274 let y = if bounds.height < scaled_size.height {
275 (state.starting_offset.y - delta.y)
276 .clamp(-hidden_height, hidden_height)
277 } else {
278 0.0
279 };
280
281 state.current_offset = Vector::new(x, y);
282 shell.request_redraw();
283 shell.capture_event();
284 }
285 }
286 _ => {}
287 }
288 }
289
290 fn mouse_interaction(
291 &self,
292 tree: &Tree,
293 layout: Layout<'_>,
294 cursor: mouse::Cursor,
295 _viewport: &Rectangle,
296 _renderer: &Renderer,
297 ) -> mouse::Interaction {
298 let state = tree.state.downcast_ref::<State>();
299 let bounds = layout.bounds();
300 let is_mouse_over = cursor.is_over(bounds);
301
302 if state.is_cursor_grabbed() {
303 mouse::Interaction::Grabbing
304 } else if is_mouse_over {
305 mouse::Interaction::Grab
306 } else {
307 mouse::Interaction::None
308 }
309 }
310
311 fn draw(
312 &self,
313 tree: &Tree,
314 renderer: &mut Renderer,
315 _theme: &Theme,
316 _style: &renderer::Style,
317 layout: Layout<'_>,
318 _cursor: mouse::Cursor,
319 viewport: &Rectangle,
320 ) {
321 let state = tree.state.downcast_ref::<State>();
322 let bounds = layout.bounds();
323
324 let final_size = scaled_image_size(
325 renderer,
326 &self.handle,
327 state,
328 bounds.size(),
329 self.content_fit,
330 );
331
332 let translation = {
333 let diff_w = bounds.width - final_size.width;
334 let diff_h = bounds.height - final_size.height;
335
336 let image_top_left = match self.content_fit {
337 ContentFit::None => {
338 Vector::new(diff_w.max(0.0) / 2.0, diff_h.max(0.0) / 2.0)
339 }
340 _ => Vector::new(diff_w / 2.0, diff_h / 2.0),
341 };
342
343 image_top_left - state.offset(bounds, final_size)
344 };
345
346 let drawing_bounds = Rectangle::new(bounds.position(), final_size);
347
348 let render = |renderer: &mut Renderer| {
349 renderer.with_translation(translation, |renderer| {
350 renderer.draw_image(
351 Image {
352 handle: self.handle.clone(),
353 border_radius: border::Radius::default(),
354 filter_method: self.filter_method,
355 rotation: Radians(0.0),
356 opacity: 1.0,
357 snap: true,
358 },
359 drawing_bounds,
360 *viewport,
361 );
362 });
363 };
364
365 renderer.with_layer(bounds, render);
366 }
367}
368
369#[derive(Debug, Clone, Copy)]
371pub struct State {
372 scale: f32,
373 starting_offset: Vector,
374 current_offset: Vector,
375 cursor_grabbed_at: Option<Point>,
376}
377
378impl Default for State {
379 fn default() -> Self {
380 Self {
381 scale: 1.0,
382 starting_offset: Vector::default(),
383 current_offset: Vector::default(),
384 cursor_grabbed_at: None,
385 }
386 }
387}
388
389impl State {
390 pub fn new() -> Self {
392 State::default()
393 }
394
395 fn offset(&self, bounds: Rectangle, image_size: Size) -> Vector {
398 let hidden_width =
399 (image_size.width - bounds.width / 2.0).max(0.0).round();
400
401 let hidden_height =
402 (image_size.height - bounds.height / 2.0).max(0.0).round();
403
404 Vector::new(
405 self.current_offset.x.clamp(-hidden_width, hidden_width),
406 self.current_offset.y.clamp(-hidden_height, hidden_height),
407 )
408 }
409
410 pub fn is_cursor_grabbed(&self) -> bool {
412 self.cursor_grabbed_at.is_some()
413 }
414}
415
416impl<'a, Message, Theme, Renderer, Handle> From<Viewer<Handle>>
417 for Element<'a, Message, Theme, Renderer>
418where
419 Renderer: 'a + image::Renderer<Handle = Handle>,
420 Message: 'a,
421 Handle: Clone + 'a,
422{
423 fn from(viewer: Viewer<Handle>) -> Element<'a, Message, Theme, Renderer> {
424 Element::new(viewer)
425 }
426}
427
428pub fn scaled_image_size<Renderer>(
432 renderer: &Renderer,
433 handle: &<Renderer as image::Renderer>::Handle,
434 state: &State,
435 bounds: Size,
436 content_fit: ContentFit,
437) -> Size
438where
439 Renderer: image::Renderer,
440{
441 let Size { width, height } =
442 renderer.measure_image(handle).unwrap_or_default();
443
444 let image_size = Size::new(width as f32, height as f32);
445
446 let adjusted_fit = content_fit.fit(image_size, bounds);
447
448 Size::new(
449 adjusted_fit.width * state.scale,
450 adjusted_fit.height * state.scale,
451 )
452}