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