mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-25 02:07:09 -06:00
Fix #2995: route tab reorder through window.panel.move and harden dock_tab_order
Legacy and floating strip drags update order via the panel.move command; partial saved orders append missing tabs, with command-level tests and Illustrator workspace doc link. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
committed by
bflatastic
co-authored by
Cursor
parent
2c715c9fe3
commit
ba2a5bfaed
+32
-46
@@ -251,7 +251,7 @@ fn tab_strip(app: &mut VectorcraftApp, ui: &mut Ui, active: DockTab) -> bool {
|
||||
} else if resp.clicked() {
|
||||
app.ui.dock_tab = tab;
|
||||
}
|
||||
crate::panel_docking::legacy_tab_in_strip(app, ui, id, &resp, Some(strip));
|
||||
crate::panel_docking::legacy_tab_in_strip(app, ui, id, resp, Some(strip));
|
||||
}
|
||||
if let Some(tab) = out.picked.and_then(|i| tabs.get(i)).copied() {
|
||||
app.ui.dock_tab = tab;
|
||||
@@ -274,7 +274,9 @@ fn tab_strip(app: &mut VectorcraftApp, ui: &mut Ui, active: DockTab) -> bool {
|
||||
|
||||
/// App styling and overflow menus around the shared tab fitting/interaction components. Docking
|
||||
/// and floating groups consume the responses themselves, preserving their different drag rules.
|
||||
/// Drag a tab onto another tab in the same strip to reorder (pointer still on the strip at release).
|
||||
/// Drag a tab onto another tab in the same strip to reorder (pointer still on the strip at release),
|
||||
/// as in Illustrator's workspace basics
|
||||
/// (<https://helpx.adobe.com/illustrator/using/workspace-basics.html>).
|
||||
pub(crate) fn tab_reorder_on_release(
|
||||
strip: Rect,
|
||||
pointer: Option<Pos2>,
|
||||
@@ -284,7 +286,7 @@ pub(crate) fn tab_reorder_on_release(
|
||||
if !source.drag_stopped() {
|
||||
return None;
|
||||
}
|
||||
let Some(pointer) = pointer.filter(|p| p.is_finite()) else { return None };
|
||||
let pointer = pointer.filter(|p| p.is_finite())?;
|
||||
if !strip.expand(4.0).contains(pointer) {
|
||||
return None;
|
||||
}
|
||||
@@ -299,22 +301,12 @@ pub(crate) fn tab_reorder_on_release(
|
||||
fn apply_dock_tab_reorder(app: &mut VectorcraftApp, visible: &[DockTab], from: usize, to: usize) {
|
||||
let Some(from_tab) = visible.get(from) else { return };
|
||||
let Some(to_tab) = visible.get(to) else { return };
|
||||
if app.ui.docking.is_some() {
|
||||
if let Err(error) = app.run(
|
||||
"window.panel.move",
|
||||
json!({ "panel": from_tab.info().0, "anchor": to_tab.info().0, "before": to_tab.info().0 }),
|
||||
) {
|
||||
app.ui.status = error;
|
||||
}
|
||||
return;
|
||||
if let Err(error) = app.run(
|
||||
"window.panel.move",
|
||||
json!({ "panel": from_tab.info().0, "anchor": to_tab.info().0, "before": to_tab.info().0 }),
|
||||
) {
|
||||
app.ui.status = error;
|
||||
}
|
||||
let mut order: Vec<DockTab> = floating::docked_tabs(&app.ui).collect();
|
||||
if from >= order.len() || to >= order.len() {
|
||||
return;
|
||||
}
|
||||
let item = order.remove(from);
|
||||
order.insert(to, item);
|
||||
app.ui.dock_tab_order = order;
|
||||
}
|
||||
|
||||
pub(crate) struct PanelTabs {
|
||||
@@ -737,26 +729,6 @@ mod tests {
|
||||
egui::pos2(SCREEN.x - 10.0, HEADER / 2.0)
|
||||
}
|
||||
|
||||
fn dock_tabs(&self) -> Vec<Rect> {
|
||||
let dock = self.ctx.data(|d| d.get_temp::<Rect>(floating::dock_rect_id())).unwrap();
|
||||
let mut tabs: Vec<Rect> =
|
||||
widget_rects(&self.ctx, vec2(40.0, 32.0)).filter(|r| (31.0..=34.0).contains(&r.height()) && dock.contains_rect(*r)).collect();
|
||||
tabs.sort_by(|a, b| a.left().total_cmp(&b.left()));
|
||||
tabs
|
||||
}
|
||||
|
||||
fn drag(&mut self, from: Pos2, to: Pos2) {
|
||||
let button = |pos, pressed| {
|
||||
egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: egui::Modifiers::NONE }
|
||||
};
|
||||
self.frame(vec![egui::Event::PointerMoved(from)]);
|
||||
self.frame(vec![button(from, true)]);
|
||||
for k in 1..=8 {
|
||||
self.frame(vec![egui::Event::PointerMoved(from + (to - from) * (k as f32 / 8.0))]);
|
||||
}
|
||||
self.frame(vec![button(to, false)]);
|
||||
self.settle();
|
||||
}
|
||||
}
|
||||
|
||||
fn icon_panel_count() -> usize {
|
||||
@@ -869,15 +841,29 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dock_tab_strip_drags_reorder_tabs_horizontally() {
|
||||
let mut h = Harness::new();
|
||||
let tabs = h.dock_tabs();
|
||||
assert_eq!(tabs.len(), 3);
|
||||
h.drag(tabs[2].center(), tabs[0].center());
|
||||
let order: Vec<_> = floating::docked_tabs(&h.app.ui).map(|t| t.info().0).collect();
|
||||
fn window_panel_move_reorders_legacy_dock_tabs_and_persists_order() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 300, "height": 300})).unwrap();
|
||||
assert!(app.ui.docking.is_none());
|
||||
app.run(
|
||||
"window.panel.move",
|
||||
json!({ "panel": "libraries", "anchor": "properties", "before": "properties" }),
|
||||
)
|
||||
.unwrap();
|
||||
let order: Vec<_> = floating::docked_tabs(&app.ui).map(|t| t.info().0).collect();
|
||||
assert_eq!(order, ["libraries", "properties", "layers"]);
|
||||
let saved: crate::state::UiState = serde_json::from_slice(&serde_json::to_vec(&h.app.ui).unwrap()).unwrap();
|
||||
assert_eq!(saved.dock_tab_order, h.app.ui.dock_tab_order);
|
||||
let saved: crate::state::UiState = serde_json::from_slice(&serde_json::to_vec(&app.ui).unwrap()).unwrap();
|
||||
assert_eq!(saved.dock_tab_order, app.ui.dock_tab_order);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partial_dock_tab_order_appends_tabs_missing_from_preferences() {
|
||||
let mut ui = crate::state::UiState::default();
|
||||
ui.dock_tab_order = vec![DockTab::Layers];
|
||||
let order = floating::resolved_dock_tab_order(&ui);
|
||||
assert_eq!(order.len(), DockTab::ALL.len());
|
||||
assert_eq!(order[0], DockTab::Layers);
|
||||
assert!(order.contains(&DockTab::Properties) && order.contains(&DockTab::Libraries));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -209,11 +209,30 @@ fn ids_of(g: &FloatingPanels) -> Vec<&'static str> {
|
||||
g.panels.iter().filter_map(|p| panel(p)).map(|(id, _)| id).collect()
|
||||
}
|
||||
|
||||
/// Saved dock tab order, appending any tabs missing from older preferences.
|
||||
pub(crate) fn resolved_dock_tab_order(ui: &UiState) -> Vec<DockTab> {
|
||||
if ui.dock_tab_order.is_empty() {
|
||||
DockTab::ALL.to_vec()
|
||||
} else {
|
||||
let mut order = ui.dock_tab_order.clone();
|
||||
for tab in DockTab::ALL {
|
||||
if !order.contains(&tab) {
|
||||
order.push(tab);
|
||||
}
|
||||
}
|
||||
order
|
||||
}
|
||||
}
|
||||
|
||||
/// Persist dock tab order when it differs from the default visible strip.
|
||||
pub(crate) fn set_dock_tab_order(ui: &mut UiState, visible: Vec<DockTab>) {
|
||||
let default = DockTab::ALL.iter().copied().filter(|t| group_of(ui, t.info().0).is_none()).collect::<Vec<_>>();
|
||||
ui.dock_tab_order = if visible == default { Vec::new() } else { visible };
|
||||
}
|
||||
|
||||
/// The dock tabs left in the dock, in tab order.
|
||||
pub fn docked_tabs(ui: &UiState) -> impl Iterator<Item = DockTab> + '_ {
|
||||
let order: Vec<DockTab> =
|
||||
if ui.dock_tab_order.is_empty() { DockTab::ALL.to_vec() } else { ui.dock_tab_order.clone() };
|
||||
order.into_iter().filter(|t| group_of(ui, t.info().0).is_none())
|
||||
resolved_dock_tab_order(ui).into_iter().filter(|t| group_of(ui, t.info().0).is_none())
|
||||
}
|
||||
|
||||
/// The dock tab the tabbed group shows: the chosen one, or the first left when it floats.
|
||||
@@ -659,15 +678,16 @@ fn apply(app: &mut VectorcraftApp, ctx: &egui::Context, action: Option<Action>)
|
||||
Some(Action::Detach(gi, left)) => detach(app, ctx, gi, left),
|
||||
Some(Action::Tear(id, left)) => tear(app, ctx, &[id], id, strip_grab(ctx, left)),
|
||||
Some(Action::Reorder(gi, from, to)) => {
|
||||
if let Some(g) = app.ui.floating_panels.get_mut(gi)
|
||||
&& from < g.panels.len()
|
||||
&& to < g.panels.len()
|
||||
&& from != to
|
||||
let ids = app.ui.floating_panels.get(gi).and_then(|g| {
|
||||
if from == to { None } else { Some((g.panels.get(from).cloned(), g.panels.get(to).cloned())) }
|
||||
});
|
||||
if let Some((Some(from_id), Some(to_id))) = ids
|
||||
&& let Err(error) = app.run(
|
||||
"window.panel.move",
|
||||
json!({ "panel": from_id, "anchor": to_id, "before": to_id }),
|
||||
)
|
||||
{
|
||||
let active = g.panels.get(g.active).cloned();
|
||||
let panel = g.panels.remove(from);
|
||||
g.panels.insert(to.min(g.panels.len()), panel);
|
||||
g.active = active.and_then(|id| g.panels.iter().position(|p| *p == id)).unwrap_or(0);
|
||||
app.ui.status = error;
|
||||
}
|
||||
}
|
||||
Some(Action::Undock(gi, grab)) => {
|
||||
|
||||
@@ -142,6 +142,41 @@ fn number(params: &Value, key: &str, default: f32) -> Result<f32, String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Legacy workspaces (no saved `docking` layout): reorder tabs in the dock strip or a floating
|
||||
/// group without promoting to a shared layout.
|
||||
fn legacy_same_strip_reorder(app: &mut VectorcraftApp, panel: &str, before: &str) -> Result<Value, String> {
|
||||
use crate::state::DockTab;
|
||||
if crate::state::DockTab::from_id(panel).is_some()
|
||||
&& DockTab::from_id(before).is_some()
|
||||
&& crate::floating::group_of(&app.ui, panel).is_none()
|
||||
&& crate::floating::group_of(&app.ui, before).is_none()
|
||||
{
|
||||
let mut order: Vec<DockTab> = crate::floating::docked_tabs(&app.ui).collect();
|
||||
let from = order.iter().position(|t| t.info().0 == panel).ok_or("panel is not in the dock strip")?;
|
||||
let to = order.iter().position(|t| t.info().0 == before).ok_or("before is not in the dock strip")?;
|
||||
if from != to {
|
||||
let item = order.remove(from);
|
||||
order.insert(to, item);
|
||||
crate::floating::set_dock_tab_order(&mut app.ui, order);
|
||||
}
|
||||
return Ok(json!({ "panel": panel, "before": before }));
|
||||
}
|
||||
let gi = crate::floating::group_of(&app.ui, panel).ok_or("panel is not in a tab group")?;
|
||||
if crate::floating::group_of(&app.ui, before) != Some(gi) {
|
||||
return Err("before must name a tab in the same group".into());
|
||||
}
|
||||
let g = app.ui.floating_panels.get_mut(gi).ok_or("panel group missing")?;
|
||||
let from = g.panels.iter().position(|id| id == panel).ok_or("panel is not in the group")?;
|
||||
let to = g.panels.iter().position(|id| id == before).ok_or("before is not in the group")?;
|
||||
if from != to {
|
||||
let active = g.panels.get(g.active).cloned();
|
||||
let moved = g.panels.remove(from);
|
||||
g.panels.insert(to, moved);
|
||||
g.active = active.and_then(|id| g.panels.iter().position(|p| *p == id)).unwrap_or(0);
|
||||
}
|
||||
Ok(json!({ "panel": panel, "before": before }))
|
||||
}
|
||||
|
||||
fn placement(params: &Value) -> Result<Placement<String>, String> {
|
||||
if let Some(before) = params.get("before") {
|
||||
let id = before.as_str().and_then(normalize).ok_or("before must name a panel")?;
|
||||
@@ -294,6 +329,10 @@ fn apply_action(app: &mut VectorcraftApp, action: Action<String>) -> Result<Valu
|
||||
|
||||
fn change(app: &mut VectorcraftApp, operation: &str, params: &Value) -> Result<Value, String> {
|
||||
let panel = params.get("panel").and_then(Value::as_str).and_then(normalize).ok_or("panel must name a registered panel")?;
|
||||
if app.ui.docking.is_none() && operation == "move" && params.get("before").is_some() {
|
||||
let before = params.get("before").and_then(Value::as_str).and_then(normalize).ok_or("before must name a panel")?;
|
||||
return legacy_same_strip_reorder(app, panel, before);
|
||||
}
|
||||
if app.ui.docking.as_ref().is_some_and(|layout| !valid(layout)) {
|
||||
return Err("saved panel layout is invalid; reset the workspace first".into());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user