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