Skip to content

Commit 3aa3252

Browse files
committed
feat(studio): add real-time drag-to-move, corner span resizing, and interactive gridline adjusters
1 parent e1c4cb7 commit 3aa3252

2 files changed

Lines changed: 163 additions & 16 deletions

File tree

Cargo.lock

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/embedded-gui-studio/src/main.rs

Lines changed: 159 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)