@@ -13,6 +13,28 @@ live_design! {
1313 use crate :: shared:: styles:: * ;
1414 use crate :: shared:: icon_button:: RobrixIconButton ;
1515 IMG_DEFAULT = dep( "crate://self/resources/img/default_image.png" )
16+
17+ ZoomableImage = { { ZoomableImage } } {
18+ width: Fill ,
19+ height: Fill ,
20+ flow: Overlay ,
21+
22+ // Transform state
23+ scale: 1.0 ,
24+ offset: ( 0.0 , 0.0 ) ,
25+
26+ // Interaction state
27+ dragging: false ,
28+ last_mouse_pos: ( 0.0 , 0.0 ) ,
29+
30+ // Internal image widget to hold the actual texture
31+ inner_image = <Image > {
32+ width: Fill ,
33+ height: Fill ,
34+ fit: Smallest ,
35+ source: ( IMG_DEFAULT )
36+ }
37+ }
1638 pub ImageViewerModal = { { ImageViewerModal } } {
1739 width: Fill , height: Fill
1840 image_modal = <Modal > {
@@ -31,13 +53,11 @@ live_design! {
3153 image_container = <View > {
3254 width: Fill ,
3355 height: Fill ,
34- flow: Down ,
35- align: { x: 0.5 }
36- image = <Image > {
37- height: Fill
38- width: Fill
39- fit: Smallest ,
40- source: ( IMG_DEFAULT )
56+ flow: Overlay ,
57+ align: { x: 0.5 , y: 0.5 }
58+ zoomable_image = <ZoomableImage > {
59+ width: Fill ,
60+ height: Fill ,
4161 }
4262 }
4363 <View > {
@@ -130,6 +150,120 @@ pub enum ImageViewerModalAction {
130150 None ,
131151}
132152
153+ #[ derive( Live , LiveHook , Widget ) ]
154+ pub struct ZoomableImage {
155+ #[ deref]
156+ view : View ,
157+
158+ // Transform state
159+ #[ live]
160+ scale : f64 ,
161+ #[ live]
162+ offset : DVec2 ,
163+
164+ // Interaction state
165+ #[ live]
166+ dragging : bool ,
167+ #[ live]
168+ last_mouse_pos : DVec2 ,
169+ }
170+
171+ impl Widget for ZoomableImage {
172+ fn handle_event ( & mut self , cx : & mut Cx , event : & Event , scope : & mut Scope ) {
173+ self . view . handle_event ( cx, event, scope) ;
174+
175+ match event {
176+ Event :: MouseDown ( e) => {
177+ if e. button . is_primary ( ) {
178+ self . dragging = true ;
179+ self . last_mouse_pos = e. abs ;
180+ cx. set_key_focus ( self . view . area ( ) ) ;
181+ self . redraw ( cx) ;
182+ }
183+ }
184+ Event :: MouseUp ( e) => {
185+ if e. button . is_primary ( ) {
186+ self . dragging = false ;
187+ self . redraw ( cx) ;
188+ }
189+ }
190+ Event :: MouseMove ( e) => {
191+ if self . dragging {
192+ let delta = e. abs - self . last_mouse_pos ;
193+
194+ // Scale delta by inverse of zoom to maintain consistent pan speed
195+ let scaled_delta = delta / self . scale ;
196+ self . offset += scaled_delta;
197+
198+ self . last_mouse_pos = e. abs ;
199+ self . redraw ( cx) ;
200+ }
201+ }
202+ Event :: Scroll ( e) => {
203+ // Zoom in/out with mouse wheel
204+ let zoom_factor = if e. scroll . y > 0.0 { 1.1 } else { 0.9 } ;
205+ let old_scale = self . scale ;
206+ self . scale *= zoom_factor;
207+ self . scale = self . scale . clamp ( 0.1 , 10.0 ) ; // Clamp zoom level
208+
209+ if self . scale != old_scale {
210+ // Get widget's rect
211+ let widget_rect = self . view . area ( ) . rect ( cx) ;
212+ let widget_center = DVec2 {
213+ x : widget_rect. pos . x as f64 + widget_rect. size . x as f64 * 0.5 ,
214+ y : widget_rect. pos . y as f64 + widget_rect. size . y as f64 * 0.5 ,
215+ } ;
216+
217+ // Calculate mouse position relative to widget center
218+ let mouse_rel = e. abs - widget_center;
219+
220+ // Adjust offset to zoom toward mouse position
221+ let scale_ratio = self . scale / old_scale;
222+ self . offset = self . offset * scale_ratio + mouse_rel * ( 1.0 - scale_ratio) / self . scale ;
223+
224+ self . redraw ( cx) ;
225+ }
226+ }
227+ _ => { }
228+ }
229+ }
230+
231+ fn draw_walk ( & mut self , cx : & mut Cx2d , scope : & mut Scope , walk : Walk ) -> DrawStep {
232+ // Apply transform by modifying the drawing context
233+ // This is a simplified approach - in a full implementation, we'd use custom shaders
234+ if self . scale != 1.0 || self . offset . x != 0.0 || self . offset . y != 0.0 {
235+ // Update the inner image's transform properties if possible
236+ // For now, just redraw - the actual transform will be visual feedback
237+ self . view . redraw ( cx) ;
238+ }
239+ self . view . draw_walk ( cx, scope, walk)
240+ }
241+ }
242+
243+ impl ZoomableImage {
244+ pub fn reset_transform ( & mut self , cx : & mut Cx ) {
245+ self . scale = 1.0 ;
246+ self . offset = DVec2 { x : 0.0 , y : 0.0 } ;
247+ self . dragging = false ;
248+ self . redraw ( cx) ;
249+ }
250+
251+ pub fn set_image_texture ( & mut self , cx : & mut Cx , data : & [ u8 ] ) -> Result < ( ) , String > {
252+ // Load the image data into the internal image widget
253+ let image = self . view . image ( id ! ( inner_image) ) ;
254+ match crate :: utils:: load_png_or_jpg ( & image, cx, data) {
255+ Ok ( ( ) ) => {
256+ // Reset transform when setting new image
257+ self . reset_transform ( cx) ;
258+ Ok ( ( ) )
259+ }
260+ Err ( e) => {
261+ Err ( format ! ( "Failed to load image: {:?}" , e) )
262+ }
263+ }
264+ }
265+ }
266+
133267impl Widget for ImageViewerModal {
134268 fn handle_event ( & mut self , cx : & mut Cx , event : & Event , scope : & mut Scope ) {
135269 self . view . handle_event ( cx, event, scope) ;
@@ -187,18 +321,19 @@ impl ImageViewerModal {
187321 }
188322 }
189323
190- /// Load image data into the Image widget
324+ /// Load image data into the ZoomableImage widget
191325 fn load_image_data ( & mut self , cx : & mut Cx , data : & [ u8 ] ) {
192- let image = self . view . image ( id ! ( image) ) ;
193- match load_png_or_jpg ( & image, cx, data) {
194- Ok ( ( ) ) => {
195- self . view . view ( id ! ( loading_view) ) . set_visible ( cx, false ) ;
196- self . view . view ( id ! ( error_label_view) ) . set_visible ( cx, false ) ;
197- self . view . image ( id ! ( image) ) . set_visible ( cx, true ) ;
198- }
199- Err ( e) => {
200- error ! ( "Failed to load image: {:?}" , e) ;
201- self . show_error_state ( cx) ;
326+ println ! ( "load_image_data" ) ;
327+ if let Some ( mut zoomable_image) = self . view . zoomable_image ( id ! ( zoomable_image) ) . borrow_mut ( ) {
328+ match zoomable_image. set_image_texture ( cx, data) {
329+ Ok ( ( ) ) => {
330+ self . view . view ( id ! ( loading_view) ) . set_visible ( cx, false ) ;
331+ self . view . view ( id ! ( error_label_view) ) . set_visible ( cx, false ) ;
332+ self . view . view ( id ! ( image_container) ) . set_visible ( cx, true ) ;
333+ }
334+ Err ( e) => {
335+ self . show_error_state ( cx) ;
336+ }
202337 }
203338 }
204339 }
@@ -207,18 +342,24 @@ impl ImageViewerModal {
207342 fn show_loading_state ( & mut self , cx : & mut Cx ) {
208343 self . view . view ( id ! ( loading_view) ) . set_visible ( cx, true ) ;
209344 self . view . view ( id ! ( error_label_view) ) . set_visible ( cx, false ) ;
210- self . view . image ( id ! ( image ) ) . set_visible ( cx, false ) ;
345+ self . view . view ( id ! ( image_container ) ) . set_visible ( cx, false ) ;
211346 }
212347
213348 /// Show error state
214349 fn show_error_state ( & mut self , cx : & mut Cx ) {
215350 self . view . view ( id ! ( loading_view) ) . set_visible ( cx, false ) ;
216351 self . view . view ( id ! ( error_label_view) ) . set_visible ( cx, true ) ;
217- self . view . image ( id ! ( image ) ) . set_visible ( cx, false ) ;
352+ self . view . view ( id ! ( image_container ) ) . set_visible ( cx, false ) ;
218353 }
219354 fn close ( & mut self , cx : & mut Cx ) {
220355 self . mxc_uri = None ;
221356 self . image_loaded = false ;
357+
358+ // Reset the zoomable image transform
359+ if let Some ( mut zoomable_image) = self . view . zoomable_image ( id ! ( zoomable_image) ) . borrow_mut ( ) {
360+ zoomable_image. reset_transform ( cx) ;
361+ }
362+
222363 self . modal ( id ! ( image_modal) ) . close ( cx) ;
223364 }
224365}
0 commit comments