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