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