@@ -177,7 +177,7 @@ impl EmbeddedGuiStudio {
177177 egui:: ScrollArea :: both ( ) . show ( ui, |ui| {
178178 let ( response, painter) = ui. allocate_painter (
179179 Vec2 :: new ( screen_w + 32.0 , screen_h + 32.0 ) ,
180- egui:: Sense :: click ( ) ,
180+ egui:: Sense :: click_and_drag ( ) ,
181181 ) ;
182182 let origin = response. rect . min + Vec2 :: new ( 16.0 , 16.0 ) ;
183183 let display_rect = Rect :: from_min_size ( origin, Vec2 :: new ( screen_w, screen_h) ) ;
@@ -239,9 +239,83 @@ impl EmbeddedGuiStudio {
239239
240240 let t = self . timeline_time ;
241241 let pointer_pos = ui. input ( |i| i. pointer . interact_pos ( ) ) ;
242+ let is_dragging = ui. input ( |i| i. pointer . is_decidedly_dragging ( ) ) ;
242243 let mut newly_selected = None ;
244+ let mut mutated_screen = screen. clone ( ) ;
245+ let mut did_mutate = false ;
243246
244- // Draw each widget with live animation time passed in
247+ // 1. Draw subtle interactive grid guideline dividers
248+ for ( ci, & cx) in col_xs. iter ( ) . enumerate ( ) . skip ( 1 ) {
249+ let div_x = cx - gap / 2.0 ;
250+ painter. line_segment (
251+ [
252+ Pos2 :: new ( div_x, inner_rect. min . y ) ,
253+ Pos2 :: new ( div_x, inner_rect. max . y ) ,
254+ ] ,
255+ Stroke :: new ( 1.0f32 , Color32 :: from_rgba_unmultiplied ( 60 , 80 , 110 , 80 ) ) ,
256+ ) ;
257+
258+ // Interactive column divider dragging
259+ if let Some ( pos) = pointer_pos {
260+ if ( pos. x - div_x) . abs ( ) <= 6.0
261+ && pos. y >= inner_rect. min . y
262+ && pos. y <= inner_rect. max . y
263+ {
264+ ui. ctx ( ) . set_cursor_icon ( egui:: CursorIcon :: ResizeColumn ) ;
265+ painter. line_segment (
266+ [
267+ Pos2 :: new ( div_x, inner_rect. min . y ) ,
268+ Pos2 :: new ( div_x, inner_rect. max . y ) ,
269+ ] ,
270+ Stroke :: new ( 2.0f32 , Color32 :: from_rgb ( 80 , 180 , 255 ) ) ,
271+ ) ;
272+ if is_dragging && ci > 0 && ci - 1 < mutated_screen. grid . cols . len ( ) {
273+ let new_px = ( ( pos. x - col_xs[ ci - 1 ] ) / self . preview_zoom )
274+ . max ( 20.0 )
275+ . round ( ) as u32 ;
276+ mutated_screen. grid . cols [ ci - 1 ] = GridTrackDef :: Px ( new_px) ;
277+ did_mutate = true ;
278+ }
279+ }
280+ }
281+ }
282+
283+ for ( ri, & ry) in row_ys. iter ( ) . enumerate ( ) . skip ( 1 ) {
284+ let div_y = ry - gap / 2.0 ;
285+ painter. line_segment (
286+ [
287+ Pos2 :: new ( inner_rect. min . x , div_y) ,
288+ Pos2 :: new ( inner_rect. max . x , div_y) ,
289+ ] ,
290+ Stroke :: new ( 1.0f32 , Color32 :: from_rgba_unmultiplied ( 60 , 80 , 110 , 80 ) ) ,
291+ ) ;
292+
293+ // Interactive row divider dragging
294+ if let Some ( pos) = pointer_pos {
295+ if ( pos. y - div_y) . abs ( ) <= 6.0
296+ && pos. x >= inner_rect. min . x
297+ && pos. x <= inner_rect. max . x
298+ {
299+ ui. ctx ( ) . set_cursor_icon ( egui:: CursorIcon :: ResizeRow ) ;
300+ painter. line_segment (
301+ [
302+ Pos2 :: new ( inner_rect. min . x , div_y) ,
303+ Pos2 :: new ( inner_rect. max . x , div_y) ,
304+ ] ,
305+ Stroke :: new ( 2.0f32 , Color32 :: from_rgb ( 80 , 180 , 255 ) ) ,
306+ ) ;
307+ if is_dragging && ri > 0 && ri - 1 < mutated_screen. grid . rows . len ( ) {
308+ let new_px = ( ( pos. y - row_ys[ ri - 1 ] ) / self . preview_zoom )
309+ . max ( 16.0 )
310+ . round ( ) as u32 ;
311+ mutated_screen. grid . rows [ ri - 1 ] = GridTrackDef :: Px ( new_px) ;
312+ did_mutate = true ;
313+ }
314+ }
315+ }
316+ }
317+
318+ // 2. Draw each widget & handle selection / dragging
245319 for ( idx, ( placement, widget) ) in screen. grid . children . iter ( ) . enumerate ( ) {
246320 let c = placement. col . min ( col_xs. len ( ) . saturating_sub ( 1 ) ) ;
247321 let r = placement. row . min ( row_ys. len ( ) . saturating_sub ( 1 ) ) ;
@@ -285,7 +359,7 @@ impl EmbeddedGuiStudio {
285359 // Draw widget representation
286360 draw_animated_widget ( & painter, widget_rect, widget, t) ;
287361
288- // Selection highlight & bounding box handles
362+ // Selection highlight, bounding box, and live drag-and-drop
289363 if self . selected_widget_idx == Some ( idx) {
290364 let select_stroke = Stroke :: new ( 2.0f32 , Color32 :: from_rgb ( 60 , 160 , 255 ) ) ;
291365 painter. rect_stroke (
@@ -295,15 +369,18 @@ impl EmbeddedGuiStudio {
295369 StrokeKind :: Outside ,
296370 ) ;
297371
298- // Draw 4 corner handles
299- let handle_size = 5.0 ;
300- let corners = [
372+ // Corner resize handles
373+ let handle_size = 6.0 ;
374+ let br_corner = widget_rect. right_bottom ( ) ;
375+ let br_handle_rect =
376+ Rect :: from_center_size ( br_corner, Vec2 :: splat ( handle_size + 4.0 ) ) ;
377+
378+ // Other corner handles
379+ for corner in [
301380 widget_rect. left_top ( ) ,
302381 widget_rect. right_top ( ) ,
303382 widget_rect. left_bottom ( ) ,
304- widget_rect. right_bottom ( ) ,
305- ] ;
306- for corner in corners {
383+ ] {
307384 let h_rect = Rect :: from_center_size ( corner, Vec2 :: splat ( handle_size) ) ;
308385 painter. rect_filled (
309386 h_rect,
@@ -312,16 +389,82 @@ impl EmbeddedGuiStudio {
312389 ) ;
313390 }
314391
392+ // Bottom-right handle (drag to span)
393+ painter. rect_filled (
394+ Rect :: from_center_size ( br_corner, Vec2 :: splat ( handle_size) ) ,
395+ CornerRadius :: same ( 1 ) ,
396+ Color32 :: from_rgb ( 80 , 220 , 120 ) ,
397+ ) ;
398+
399+ // Interactive handle resizing / drag moving
400+ if let Some ( pos) = pointer_pos {
401+ if br_handle_rect. contains ( pos) {
402+ ui. ctx ( ) . set_cursor_icon ( egui:: CursorIcon :: ResizeNwSe ) ;
403+ if is_dragging {
404+ // Compute target col and row spans
405+ let mut target_c = c;
406+ for ( ci, & cx) in col_xs. iter ( ) . enumerate ( ) {
407+ if pos. x >= cx {
408+ target_c = ci;
409+ }
410+ }
411+ let mut target_r = r;
412+ for ( ri, & ry) in row_ys. iter ( ) . enumerate ( ) {
413+ if pos. y >= ry {
414+ target_r = ri;
415+ }
416+ }
417+ let new_c_span = ( target_c. saturating_sub ( c) + 1 ) . max ( 1 ) ;
418+ let new_r_span = ( target_r. saturating_sub ( r) + 1 ) . max ( 1 ) ;
419+ if new_c_span != placement. col_span
420+ || new_r_span != placement. row_span
421+ {
422+ mutated_screen. grid . children [ idx] . 0 . col_span = new_c_span;
423+ mutated_screen. grid . children [ idx] . 0 . row_span = new_r_span;
424+ did_mutate = true ;
425+ }
426+ }
427+ } else if widget_rect. contains ( pos) {
428+ ui. ctx ( ) . set_cursor_icon ( egui:: CursorIcon :: Grab ) ;
429+ if is_dragging {
430+ ui. ctx ( ) . set_cursor_icon ( egui:: CursorIcon :: Grabbing ) ;
431+ // Drag-to-move widget to target cell
432+ let mut target_c = 0 ;
433+ for ( ci, & cx) in col_xs. iter ( ) . enumerate ( ) {
434+ if pos. x >= cx {
435+ target_c = ci;
436+ }
437+ }
438+ let mut target_r = 0 ;
439+ for ( ri, & ry) in row_ys. iter ( ) . enumerate ( ) {
440+ if pos. y >= ry {
441+ target_r = ri;
442+ }
443+ }
444+ target_c = target_c. min ( col_xs. len ( ) . saturating_sub ( 1 ) ) ;
445+ target_r = target_r. min ( row_ys. len ( ) . saturating_sub ( 1 ) ) ;
446+
447+ if target_c != placement. col || target_r != placement. row {
448+ mutated_screen. grid . children [ idx] . 0 . col = target_c;
449+ mutated_screen. grid . children [ idx] . 0 . row = target_r;
450+ did_mutate = true ;
451+ }
452+ }
453+ }
454+ }
455+
315456 // Floating selection badge
316457 let badge_text = format ! (
317- "🎯 {} [c:{}, r:{}]" ,
458+ "🎯 {} [c:{}, r:{}, span:{}x{} ]" ,
318459 widget. id( ) . unwrap_or( "widget" ) ,
319460 placement. col,
320- placement. row
461+ placement. row,
462+ placement. col_span,
463+ placement. row_span
321464 ) ;
322465 let badge_pos = Pos2 :: new ( widget_rect. min . x , widget_rect. min . y - 14.0 ) ;
323466 painter. rect_filled (
324- Rect :: from_min_size ( badge_pos, Vec2 :: new ( 120 .0, 14.0 ) ) ,
467+ Rect :: from_min_size ( badge_pos, Vec2 :: new ( 160 .0, 14.0 ) ) ,
325468 CornerRadius :: same ( 3 ) ,
326469 Color32 :: from_rgb ( 30 , 80 , 180 ) ,
327470 ) ;
@@ -338,6 +481,10 @@ impl EmbeddedGuiStudio {
338481 if let Some ( sel) = newly_selected {
339482 self . selected_widget_idx = Some ( sel) ;
340483 }
484+
485+ if did_mutate {
486+ self . sync_from_screen ( & mutated_screen) ;
487+ }
341488 } ) ;
342489 }
343490
0 commit comments