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