1use crate::core::image::{self, FilterMethod};
3use crate::core::layout;
4use crate::core::mouse;
5use crate::core::renderer;
6use crate::core::widget::tree::{self, Tree};
7use crate::core::{
8 Clipboard, ContentFit, Element, Event, Image, Layout, Length, Pixels,
9 Point, Radians, Rectangle, Shell, Size, Vector, Widget,
10};
11
12#[allow(missing_debug_implementations)]
14pub 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 &self,
121 _tree: &mut Tree,
122 renderer: &Renderer,
123 limits: &layout::Limits,
124 ) -> layout::Node {
125 let image_size = renderer.measure_image(&self.handle);
127 let image_size =
128 Size::new(image_size.width as f32, image_size.height as f32);
129
130 let raw_size = limits.resolve(self.width, self.height, image_size);
132
133 let full_size = self.content_fit.fit(image_size, raw_size);
135
136 let final_size = Size {
138 width: match self.width {
139 Length::Shrink => f32::min(raw_size.width, full_size.width),
140 _ => raw_size.width,
141 },
142 height: match self.height {
143 Length::Shrink => f32::min(raw_size.height, full_size.height),
144 _ => raw_size.height,
145 },
146 };
147
148 layout::Node::new(final_size)
149 }
150
151 fn update(
152 &mut self,
153 tree: &mut Tree,
154 event: &Event,
155 layout: Layout<'_>,
156 cursor: mouse::Cursor,
157 renderer: &Renderer,
158 _clipboard: &mut dyn Clipboard,
159 shell: &mut Shell<'_, Message>,
160 _viewport: &Rectangle,
161 ) {
162 let bounds = layout.bounds();
163
164 match event {
165 Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
166 let Some(cursor_position) = cursor.position_over(bounds) else {
167 return;
168 };
169
170 match *delta {
171 mouse::ScrollDelta::Lines { y, .. }
172 | mouse::ScrollDelta::Pixels { y, .. } => {
173 let state = tree.state.downcast_mut::<State>();
174 let previous_scale = state.scale;
175
176 if y < 0.0 && previous_scale > self.min_scale
177 || y > 0.0 && previous_scale < self.max_scale
178 {
179 state.scale = (if y > 0.0 {
180 state.scale * (1.0 + self.scale_step)
181 } else {
182 state.scale / (1.0 + self.scale_step)
183 })
184 .clamp(self.min_scale, self.max_scale);
185
186 let scaled_size = scaled_image_size(
187 renderer,
188 &self.handle,
189 state,
190 bounds.size(),
191 self.content_fit,
192 );
193
194 let factor = state.scale / previous_scale - 1.0;
195
196 let cursor_to_center =
197 cursor_position - bounds.center();
198
199 let adjustment = cursor_to_center * factor
200 + state.current_offset * factor;
201
202 state.current_offset = Vector::new(
203 if scaled_size.width > bounds.width {
204 state.current_offset.x + adjustment.x
205 } else {
206 0.0
207 },
208 if scaled_size.height > bounds.height {
209 state.current_offset.y + adjustment.y
210 } else {
211 0.0
212 },
213 );
214 }
215 }
216 }
217
218 shell.request_redraw();
219 shell.capture_event();
220 }
221 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
222 let Some(cursor_position) = cursor.position_over(bounds) else {
223 return;
224 };
225
226 let state = tree.state.downcast_mut::<State>();
227
228 state.cursor_grabbed_at = Some(cursor_position);
229 state.starting_offset = state.current_offset;
230
231 shell.request_redraw();
232 shell.capture_event();
233 }
234 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
235 let state = tree.state.downcast_mut::<State>();
236
237 if state.cursor_grabbed_at.is_some() {
238 state.cursor_grabbed_at = None;
239 shell.request_redraw();
240 shell.capture_event();
241 }
242 }
243 Event::Mouse(mouse::Event::CursorMoved { position }) => {
244 let state = tree.state.downcast_mut::<State>();
245
246 if let Some(origin) = state.cursor_grabbed_at {
247 let scaled_size = scaled_image_size(
248 renderer,
249 &self.handle,
250 state,
251 bounds.size(),
252 self.content_fit,
253 );
254 let hidden_width = (scaled_size.width - bounds.width / 2.0)
255 .max(0.0)
256 .round();
257
258 let hidden_height = (scaled_size.height
259 - bounds.height / 2.0)
260 .max(0.0)
261 .round();
262
263 let delta = *position - origin;
264
265 let x = if bounds.width < scaled_size.width {
266 (state.starting_offset.x - delta.x)
267 .clamp(-hidden_width, hidden_width)
268 } else {
269 0.0
270 };
271
272 let y = if bounds.height < scaled_size.height {
273 (state.starting_offset.y - delta.y)
274 .clamp(-hidden_height, hidden_height)
275 } else {
276 0.0
277 };
278
279 state.current_offset = Vector::new(x, y);
280 shell.request_redraw();
281 shell.capture_event();
282 }
283 }
284 _ => {}
285 }
286 }
287
288 fn mouse_interaction(
289 &self,
290 tree: &Tree,
291 layout: Layout<'_>,
292 cursor: mouse::Cursor,
293 _viewport: &Rectangle,
294 _renderer: &Renderer,
295 ) -> mouse::Interaction {
296 let state = tree.state.downcast_ref::<State>();
297 let bounds = layout.bounds();
298 let is_mouse_over = cursor.is_over(bounds);
299
300 if state.is_cursor_grabbed() {
301 mouse::Interaction::Grabbing
302 } else if is_mouse_over {
303 mouse::Interaction::Grab
304 } else {
305 mouse::Interaction::None
306 }
307 }
308
309 fn draw(
310 &self,
311 tree: &Tree,
312 renderer: &mut Renderer,
313 _theme: &Theme,
314 _style: &renderer::Style,
315 layout: Layout<'_>,
316 _cursor: mouse::Cursor,
317 _viewport: &Rectangle,
318 ) {
319 let state = tree.state.downcast_ref::<State>();
320 let bounds = layout.bounds();
321
322 let final_size = scaled_image_size(
323 renderer,
324 &self.handle,
325 state,
326 bounds.size(),
327 self.content_fit,
328 );
329
330 let translation = {
331 let diff_w = bounds.width - final_size.width;
332 let diff_h = bounds.height - final_size.height;
333
334 let image_top_left = match self.content_fit {
335 ContentFit::None => {
336 Vector::new(diff_w.max(0.0) / 2.0, diff_h.max(0.0) / 2.0)
337 }
338 _ => Vector::new(diff_w / 2.0, diff_h / 2.0),
339 };
340
341 image_top_left - state.offset(bounds, final_size)
342 };
343
344 let drawing_bounds = Rectangle::new(bounds.position(), final_size);
345
346 let render = |renderer: &mut Renderer| {
347 renderer.with_translation(translation, |renderer| {
348 renderer.draw_image(
349 Image {
350 handle: self.handle.clone(),
351 filter_method: self.filter_method,
352 rotation: Radians(0.0),
353 opacity: 1.0,
354 snap: true,
355 },
356 drawing_bounds,
357 );
358 });
359 };
360
361 renderer.with_layer(bounds, render);
362 }
363}
364
365#[derive(Debug, Clone, Copy)]
367pub struct State {
368 scale: f32,
369 starting_offset: Vector,
370 current_offset: Vector,
371 cursor_grabbed_at: Option<Point>,
372}
373
374impl Default for State {
375 fn default() -> Self {
376 Self {
377 scale: 1.0,
378 starting_offset: Vector::default(),
379 current_offset: Vector::default(),
380 cursor_grabbed_at: None,
381 }
382 }
383}
384
385impl State {
386 pub fn new() -> Self {
388 State::default()
389 }
390
391 fn offset(&self, bounds: Rectangle, image_size: Size) -> Vector {
394 let hidden_width =
395 (image_size.width - bounds.width / 2.0).max(0.0).round();
396
397 let hidden_height =
398 (image_size.height - bounds.height / 2.0).max(0.0).round();
399
400 Vector::new(
401 self.current_offset.x.clamp(-hidden_width, hidden_width),
402 self.current_offset.y.clamp(-hidden_height, hidden_height),
403 )
404 }
405
406 pub fn is_cursor_grabbed(&self) -> bool {
408 self.cursor_grabbed_at.is_some()
409 }
410}
411
412impl<'a, Message, Theme, Renderer, Handle> From<Viewer<Handle>>
413 for Element<'a, Message, Theme, Renderer>
414where
415 Renderer: 'a + image::Renderer<Handle = Handle>,
416 Message: 'a,
417 Handle: Clone + 'a,
418{
419 fn from(viewer: Viewer<Handle>) -> Element<'a, Message, Theme, Renderer> {
420 Element::new(viewer)
421 }
422}
423
424pub fn scaled_image_size<Renderer>(
428 renderer: &Renderer,
429 handle: &<Renderer as image::Renderer>::Handle,
430 state: &State,
431 bounds: Size,
432 content_fit: ContentFit,
433) -> Size
434where
435 Renderer: image::Renderer,
436{
437 let Size { width, height } = renderer.measure_image(handle);
438 let image_size = Size::new(width as f32, height as f32);
439
440 let adjusted_fit = content_fit.fit(image_size, bounds);
441
442 Size::new(
443 adjusted_fit.width * state.scale,
444 adjusted_fit.height * state.scale,
445 )
446}