forked from 1b-agent/opencadstudio-upstream
iced_aw's DropDown keeps its list on screen by testing it against the viewport's width and height only, as if the viewport started at the window corner. Inside a scrollable the viewport is the visible area in window coordinates, so a field in a dock pallet saw its list overflow and the list jumped left by its own width (or up over the field); the Properties Transparency dropdown showed it. Hand the drop-down a viewport reaching from the corner to the visible area's far edges. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_014ihuqzxdUhd9UbioPn5MGu
1401 lines
54 KiB
Rust
1401 lines
54 KiB
Rust
//! Dock behaviour through the app: drags, hover, menus, and the real view.
|
||
|
||
use crate::app::{Message, OpenCADStudio};
|
||
|
||
fn fresh() -> OpenCADStudio {
|
||
let mut app = OpenCADStudio::new_for_test();
|
||
app.automation_op(r#"{"op":"new"}"#);
|
||
app
|
||
}
|
||
|
||
#[test]
|
||
fn blockpalette_pin_toggles_autocollapse_and_close_hides() {
|
||
let mut app = fresh();
|
||
app.show_block_palette = true;
|
||
let id = crate::ui::dock::PanelId::BlockPalette;
|
||
let _ = app.on_dock(crate::ui::dock::DockMsg::AutoCollapseToggle(id));
|
||
assert!(app.dock.auto_hides(id), "pin enables auto-hide");
|
||
let _ = app.on_dock(crate::ui::dock::DockMsg::AutoCollapseToggle(id));
|
||
assert!(!app.dock.auto_hides(id), "second pin disables auto-hide");
|
||
let _ = app.on_dock(crate::ui::dock::DockMsg::Close(id));
|
||
assert!(!app.show_block_palette, "close dismisses the sidebar");
|
||
}
|
||
|
||
/// A hermetic app with a 1600×900 workspace and the default dock layout
|
||
/// (Properties left, block palette right), both shown.
|
||
/// Run `f` on a thread with the stack the application's own threads get:
|
||
/// building and driving the real view is deeper than a default test thread.
|
||
/// One at a time: two headless renderers made in parallel crash the process.
|
||
fn with_stack(f: impl FnOnce() + Send + 'static) {
|
||
static REAL_VIEW: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
||
let _one = REAL_VIEW
|
||
.lock()
|
||
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
||
std::thread::Builder::new()
|
||
.stack_size(32 * 1024 * 1024)
|
||
.spawn(f)
|
||
.expect("spawn test thread")
|
||
.join()
|
||
.unwrap_or_else(|e| std::panic::resume_unwind(e));
|
||
}
|
||
|
||
fn dock_app() -> OpenCADStudio {
|
||
let mut app = fresh();
|
||
// Tests load the user's persisted config; reset the dock to a known
|
||
// state so this stays hermetic.
|
||
app.dock = Default::default();
|
||
app.dock.ensure_settings();
|
||
app.show_properties = true;
|
||
app.show_block_palette = true;
|
||
let i = app.active_tab;
|
||
app.tabs[i].scene.selection.borrow_mut().view.vp_size = (1600.0, 900.0);
|
||
app.win_size = (1600.0, 900.0).into();
|
||
app
|
||
}
|
||
|
||
/// Where the current dock drag would land, if it has started.
|
||
fn drag_target(app: &OpenCADStudio) -> Option<crate::ui::dock::DropTarget> {
|
||
app.dock_drag
|
||
.and_then(|d| d.movement())
|
||
.and_then(|(_, _, target)| target)
|
||
}
|
||
|
||
/// Press `id`'s title bar at `from`, drag to `to`, and return the target
|
||
/// shown just before release.
|
||
fn drag_panel(
|
||
app: &mut OpenCADStudio,
|
||
id: crate::ui::dock::PanelId,
|
||
from: iced::Point,
|
||
to: iced::Point,
|
||
) -> Option<crate::ui::dock::DropTarget> {
|
||
use crate::ui::dock::DockMsg;
|
||
let _ = app.on_dock(DockMsg::DockGrab(id));
|
||
let _ = app.on_dock(DockMsg::DragMove(from));
|
||
let _ = app.on_dock(DockMsg::DragMove(to));
|
||
let target = drag_target(app);
|
||
let _ = app.on_dock(DockMsg::DragRelease);
|
||
target
|
||
}
|
||
|
||
#[test]
|
||
fn dropping_on_a_pallets_top_or_bottom_half_stacks_above_or_below_it() {
|
||
use crate::app::config::DockSide;
|
||
use crate::ui::dock::{DropTarget, PanelId};
|
||
let mut app = dock_app();
|
||
let id = PanelId::BlockPalette;
|
||
assert_eq!(app.dock.location(id), Some((DockSide::Right, 0)));
|
||
let target = drag_panel(
|
||
&mut app,
|
||
id,
|
||
iced::Point::new(1500.0, 10.0),
|
||
iced::Point::new(150.0, 300.0),
|
||
);
|
||
assert_eq!(
|
||
target,
|
||
Some(DropTarget::Join {
|
||
side: DockSide::Left,
|
||
group: 0,
|
||
index: 0
|
||
})
|
||
);
|
||
assert_eq!(
|
||
app.dock.left[0].panels,
|
||
vec![PanelId::BlockPalette, PanelId::Properties]
|
||
);
|
||
assert!(app.dock.right.is_empty());
|
||
// The bottom half of a pallet stacks below it.
|
||
let target = drag_panel(
|
||
&mut app,
|
||
id,
|
||
iced::Point::new(150.0, 100.0),
|
||
iced::Point::new(150.0, 800.0),
|
||
);
|
||
assert_eq!(
|
||
target,
|
||
Some(DropTarget::Join {
|
||
side: DockSide::Left,
|
||
group: 0,
|
||
index: 2
|
||
})
|
||
);
|
||
assert_eq!(
|
||
app.dock.left[0].panels,
|
||
vec![PanelId::Properties, PanelId::BlockPalette]
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn dock_dragging_a_lone_pallet_over_its_own_group_is_a_no_op() {
|
||
use crate::app::config::DockSide;
|
||
use crate::ui::dock::{DropTarget, PanelId};
|
||
let mut app = dock_app();
|
||
let before = app.dock.clone();
|
||
// The right edge's only group holds just the block palette; drag it
|
||
// around over itself.
|
||
let target = drag_panel(
|
||
&mut app,
|
||
PanelId::BlockPalette,
|
||
iced::Point::new(1500.0, 10.0),
|
||
iced::Point::new(1450.0, 600.0),
|
||
);
|
||
assert_eq!(
|
||
target,
|
||
Some(DropTarget::Join {
|
||
side: DockSide::Right,
|
||
group: 0,
|
||
index: 1
|
||
})
|
||
);
|
||
assert_eq!(app.dock, before);
|
||
}
|
||
|
||
#[test]
|
||
fn dock_click_without_drag_changes_nothing() {
|
||
let mut app = dock_app();
|
||
let before = app.dock.clone();
|
||
let id = crate::ui::dock::PanelId::BlockPalette;
|
||
let p = iced::Point::new(1500.0, 10.0);
|
||
let target = drag_panel(&mut app, id, p, iced::Point::new(1502.0, 11.0));
|
||
assert_eq!(target, None, "movement under the threshold is a click");
|
||
assert_eq!(app.dock, before);
|
||
}
|
||
|
||
#[test]
|
||
fn dock_drop_below_the_strip_groups_starts_a_new_shown_group() {
|
||
use crate::app::config::DockSide;
|
||
use crate::ui::dock::{DropTarget, PanelId};
|
||
let mut app = dock_app();
|
||
let (_, plus_top) = app.dock_strip_layout(DockSide::Left);
|
||
let target = drag_panel(
|
||
&mut app,
|
||
PanelId::BlockPalette,
|
||
iced::Point::new(1500.0, 10.0),
|
||
iced::Point::new(18.0, plus_top + 10.0),
|
||
);
|
||
assert_eq!(
|
||
target,
|
||
Some(DropTarget::Edge {
|
||
side: DockSide::Left,
|
||
index: 1
|
||
})
|
||
);
|
||
assert_eq!(app.dock.left.len(), 2);
|
||
assert_eq!(app.dock.left[1].panels, vec![PanelId::BlockPalette]);
|
||
// The new group is the one shown: the column holds only the palette.
|
||
assert_eq!(app.dock_shown_group(DockSide::Left), Some(1));
|
||
assert_eq!(app.dock_slot_spans(DockSide::Left), vec![(0, 0.0, 900.0)]);
|
||
}
|
||
|
||
#[test]
|
||
fn dock_icon_press_switches_the_shown_group() {
|
||
use crate::app::config::DockSide;
|
||
use crate::ui::dock::{DockGroup, DockMsg, PanelId};
|
||
let mut app = dock_app();
|
||
app.show_browser = true;
|
||
app.dock.left = vec![
|
||
DockGroup::stack(vec![PanelId::Properties, PanelId::BlockPalette]),
|
||
DockGroup::single(PanelId::Browser),
|
||
];
|
||
app.dock.right.clear();
|
||
// Group 0 shows both stacked pallets.
|
||
assert_eq!(app.dock_slot_spans(DockSide::Left).len(), 2);
|
||
let _ = app.on_dock(DockMsg::IconPress(PanelId::Browser));
|
||
let _ = app.on_dock(DockMsg::DragRelease);
|
||
assert_eq!(app.dock_shown_group(DockSide::Left), Some(1));
|
||
assert_eq!(app.dock_slot_spans(DockSide::Left), vec![(0, 0.0, 900.0)]);
|
||
// Each group keeps its own width.
|
||
app.dock.set_group_width(DockSide::Left, 1, 400.0);
|
||
assert_eq!(app.dock_column_width(DockSide::Left), 400.0);
|
||
let _ = app.on_dock(DockMsg::IconPress(PanelId::BlockPalette));
|
||
let _ = app.on_dock(DockMsg::DragRelease);
|
||
assert_eq!(app.dock_column_width(DockSide::Left), 260.0);
|
||
}
|
||
|
||
#[test]
|
||
fn dock_drop_over_the_viewport_floats_the_panel() {
|
||
use crate::ui::dock::{DropTarget, PanelId};
|
||
let mut app = dock_app();
|
||
let target = drag_panel(
|
||
&mut app,
|
||
PanelId::BlockPalette,
|
||
iced::Point::new(1500.0, 10.0),
|
||
iced::Point::new(800.0, 400.0),
|
||
);
|
||
assert!(matches!(target, Some(DropTarget::Float { .. })));
|
||
assert!(app.dock.right.is_empty());
|
||
let f = app
|
||
.dock
|
||
.float_rect(PanelId::BlockPalette)
|
||
.expect("floating");
|
||
assert!(f.x > 600.0 && f.x < 800.0 && f.y > 350.0 && f.y < 400.0);
|
||
// Dragging a floating panel keeps the grab point under the pointer.
|
||
let target = drag_panel(
|
||
&mut app,
|
||
PanelId::BlockPalette,
|
||
iced::Point::new(f.x + 20.0, f.y + 10.0),
|
||
iced::Point::new(f.x + 120.0, f.y + 60.0),
|
||
);
|
||
assert_eq!(
|
||
target,
|
||
Some(DropTarget::Float {
|
||
x: f.x + 100.0,
|
||
y: f.y + 50.0
|
||
})
|
||
);
|
||
// Opening a floating panel must not re-dock it.
|
||
assert!(app.dock.is_placed(PanelId::BlockPalette));
|
||
}
|
||
|
||
#[test]
|
||
fn dock_dragging_an_icon_into_another_group_lands_between_its_icons() {
|
||
use crate::app::config::DockSide;
|
||
use crate::ui::dock::{DockGroup, DockMsg, DropTarget, PanelId, STRIP_CELL_H};
|
||
let mut app = dock_app();
|
||
app.show_browser = true;
|
||
app.dock.left = vec![
|
||
DockGroup::stack(vec![PanelId::Properties, PanelId::Browser]),
|
||
DockGroup::single(PanelId::BlockPalette),
|
||
];
|
||
app.dock.right.clear();
|
||
let (layout, _) = app.dock_strip_layout(DockSide::Left);
|
||
// Drag the Blocks icon (group 1) to between Properties and Browser.
|
||
let _ = app.on_dock(DockMsg::IconPress(PanelId::BlockPalette));
|
||
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(
|
||
20.0,
|
||
layout[1].icons_top + 10.0,
|
||
)));
|
||
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(
|
||
20.0,
|
||
layout[0].icons_top + STRIP_CELL_H + 2.0,
|
||
)));
|
||
assert_eq!(
|
||
drag_target(&app),
|
||
Some(DropTarget::Join {
|
||
side: DockSide::Left,
|
||
group: 0,
|
||
index: 1
|
||
})
|
||
);
|
||
let _ = app.on_dock(DockMsg::DragRelease);
|
||
assert_eq!(app.dock.left.len(), 1, "the emptied group goes away");
|
||
assert_eq!(
|
||
app.dock.left[0].panels,
|
||
vec![PanelId::Properties, PanelId::BlockPalette, PanelId::Browser]
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn dock_grip_drag_moves_a_whole_group() {
|
||
use crate::app::config::DockSide;
|
||
use crate::ui::dock::{DockGroup, DockMsg, PanelId};
|
||
let mut app = dock_app();
|
||
app.show_browser = true;
|
||
app.dock.left = vec![
|
||
DockGroup::stack(vec![PanelId::Properties, PanelId::Browser]),
|
||
DockGroup::single(PanelId::BlockPalette),
|
||
];
|
||
app.dock.right.clear();
|
||
let (layout, plus_top) = app.dock_strip_layout(DockSide::Left);
|
||
let _ = app.on_dock(DockMsg::GroupGrab(DockSide::Left, 0));
|
||
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(
|
||
18.0,
|
||
layout[0].top + 2.0,
|
||
)));
|
||
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(18.0, plus_top + 10.0)));
|
||
let _ = app.on_dock(DockMsg::DragRelease);
|
||
assert_eq!(app.dock.left[0].panels, vec![PanelId::BlockPalette]);
|
||
assert_eq!(
|
||
app.dock.left[1].panels,
|
||
vec![PanelId::Properties, PanelId::Browser]
|
||
);
|
||
// A group dropped over the viewport does not float or split up.
|
||
let before = app.dock.clone();
|
||
let _ = app.on_dock(DockMsg::GroupGrab(DockSide::Left, 1));
|
||
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(18.0, 300.0)));
|
||
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(800.0, 400.0)));
|
||
assert_eq!(drag_target(&app), None);
|
||
let _ = app.on_dock(DockMsg::DragRelease);
|
||
assert_eq!(app.dock, before);
|
||
}
|
||
|
||
#[test]
|
||
fn dock_closed_pallets_take_no_room_and_empty_groups_hide() {
|
||
use crate::app::config::DockSide;
|
||
use crate::ui::dock::{DockGroup, PanelId};
|
||
let mut app = dock_app();
|
||
app.dock.left = vec![
|
||
DockGroup::stack(vec![PanelId::BlockPalette, PanelId::Properties]),
|
||
DockGroup::single(PanelId::Browser),
|
||
];
|
||
app.show_block_palette = false;
|
||
app.show_browser = false;
|
||
// Only Properties is open: one listed group, full height.
|
||
assert_eq!(app.dock_visible_groups(DockSide::Left), vec![0]);
|
||
assert_eq!(app.dock_slot_spans(DockSide::Left), vec![(1, 0.0, 900.0)]);
|
||
// A shown group with nothing open falls back to one that has.
|
||
app.dock.show_group(DockSide::Left, 1);
|
||
assert_eq!(app.dock_shown_group(DockSide::Left), Some(0));
|
||
}
|
||
|
||
#[test]
|
||
fn dock_splitter_drag_changes_pallet_heights() {
|
||
use crate::app::config::DockSide;
|
||
use crate::ui::dock::{DockGroup, DockMsg, PanelId};
|
||
let mut app = dock_app();
|
||
app.dock.left = vec![DockGroup::stack(vec![
|
||
PanelId::BlockPalette,
|
||
PanelId::Properties,
|
||
])];
|
||
app.dock.right.clear();
|
||
let _ = app.on_dock(DockMsg::SplitGrab(DockSide::Left, 0, 0, 1));
|
||
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(100.0, 450.0)));
|
||
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(100.0, 675.0)));
|
||
let _ = app.on_dock(DockMsg::DragRelease);
|
||
// 225 px of a 900 px edge holding 2 weight units = 0.5 units.
|
||
let w = &app.dock.left[0].weights;
|
||
assert!((w[0] - 1.5).abs() < 1e-4 && (w[1] - 0.5).abs() < 1e-4);
|
||
let _ = app.on_dock(DockMsg::SplitReset(DockSide::Left, 0));
|
||
assert_eq!(app.dock.left[0].weights, vec![1.0, 1.0]);
|
||
}
|
||
|
||
#[test]
|
||
fn dock_hover_reveals_groups_only_on_an_auto_hiding_edge() {
|
||
use crate::app::config::DockSide;
|
||
use crate::ui::dock::{DockGroup, DockMsg, PanelId};
|
||
let mut app = dock_app();
|
||
app.show_browser = true;
|
||
app.dock.left.push(DockGroup::single(PanelId::Browser));
|
||
// The pin on any docked pallet turns auto-hide on for its edge.
|
||
let _ = app.on_dock(DockMsg::AutoCollapseToggle(PanelId::Properties));
|
||
assert!(app.dock.edge_auto_hide(DockSide::Left));
|
||
assert!(!app.dock.edge_auto_hide(DockSide::Right));
|
||
assert!(!app.dock_edge_expanded(DockSide::Left));
|
||
// Hovering an icon flies its group out.
|
||
let _ = app.on_dock(DockMsg::Hover(PanelId::Browser));
|
||
// ...but only once the pointer rests: the reveal waits for the delay.
|
||
assert!(!app.dock_edge_expanded(DockSide::Left));
|
||
let _ = app.on_dock(DockMsg::HoverSettled(app.dock_hover_gen));
|
||
assert!(app.dock_edge_expanded(DockSide::Left));
|
||
assert_eq!(app.dock_shown_group(DockSide::Left), Some(1));
|
||
// Leaving and coming back within the delay keeps it open.
|
||
let _ = app.on_dock(DockMsg::HoverExit);
|
||
let stale = app.dock_hover_gen;
|
||
let _ = app.on_dock(DockMsg::HoverStay);
|
||
let _ = app.on_dock(DockMsg::HoverSettled(stale));
|
||
assert!(app.dock_edge_expanded(DockSide::Left));
|
||
let _ = app.on_dock(DockMsg::HoverExit);
|
||
let _ = app.on_dock(DockMsg::HoverSettled(app.dock_hover_gen));
|
||
assert!(!app.dock_edge_expanded(DockSide::Left));
|
||
// Without auto-hide, hovering does not switch groups (clicks do).
|
||
let _ = app.on_dock(DockMsg::AutoCollapseToggle(PanelId::Properties));
|
||
let _ = app.on_dock(DockMsg::Hover(PanelId::Properties));
|
||
assert_eq!(app.dock_shown_group(DockSide::Left), Some(1));
|
||
assert!(app.dock_edge_expanded(DockSide::Left));
|
||
}
|
||
|
||
#[test]
|
||
fn dock_auto_hide_waits_for_an_open_layer_color_dropdown() {
|
||
use crate::ui::dock::{DockGroup, DockMsg, PanelId};
|
||
let mut app = dock_app();
|
||
app.dock.left.push(DockGroup::single(PanelId::Layers));
|
||
let _ = app.on_dock(DockMsg::AutoCollapseToggle(PanelId::Layers));
|
||
let _ = app.on_dock(DockMsg::Hover(PanelId::Layers));
|
||
let _ = app.on_dock(DockMsg::HoverSettled(app.dock_hover_gen));
|
||
assert_eq!(app.dock_peek, Some(PanelId::Layers));
|
||
// Opening a row's colour swatch, then moving onto its dropdown, reads
|
||
// as the pointer leaving the edge: the panel must stay out.
|
||
let _ = app.update(Message::LayerColorPickerToggle("0".to_string()));
|
||
assert!(app.tabs[app.active_tab].layers.color_picker_row.is_some());
|
||
let _ = app.on_dock(DockMsg::HoverExit);
|
||
let _ = app.on_dock(DockMsg::HoverSettled(app.dock_hover_gen));
|
||
assert_eq!(app.dock_peek, Some(PanelId::Layers));
|
||
let _ = app.on_dock(DockMsg::HoverSettled(app.dock_hover_gen));
|
||
assert_eq!(app.dock_peek, Some(PanelId::Layers));
|
||
// Once the dropdown closes with the pointer still away, it hides.
|
||
let _ = app.update(Message::LayerColorPickerToggle("0".to_string()));
|
||
let _ = app.on_dock(DockMsg::HoverSettled(app.dock_hover_gen));
|
||
assert_eq!(app.dock_peek, None);
|
||
}
|
||
|
||
#[test]
|
||
fn dock_float_resize_grows_the_panel() {
|
||
use crate::ui::dock::{DockMsg, FloatPanel, PanelId};
|
||
let mut app = dock_app();
|
||
app.dock.float(FloatPanel::new(
|
||
PanelId::BlockPalette,
|
||
100.0,
|
||
100.0,
|
||
260.0,
|
||
300.0,
|
||
));
|
||
let _ = app.on_dock(DockMsg::FloatResizeGrab(PanelId::BlockPalette, false));
|
||
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(360.0, 400.0)));
|
||
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(400.0, 450.0)));
|
||
let _ = app.on_dock(DockMsg::DragRelease);
|
||
let f = app.dock.float_rect(PanelId::BlockPalette).unwrap();
|
||
assert_eq!((f.w, f.h), (300.0, 350.0));
|
||
// The bottom-left grip grows the panel leftward, keeping its right edge.
|
||
let _ = app.on_dock(DockMsg::FloatResizeGrab(PanelId::BlockPalette, true));
|
||
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(100.0, 450.0)));
|
||
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(60.0, 450.0)));
|
||
let _ = app.on_dock(DockMsg::DragRelease);
|
||
let g = app.dock.float_rect(PanelId::BlockPalette).unwrap();
|
||
assert_eq!((g.x, g.w, g.h), (f.x - 40.0, 340.0, 350.0));
|
||
}
|
||
|
||
#[test]
|
||
fn dock_edge_menu_adds_and_hides_pallets() {
|
||
use crate::app::config::DockSide;
|
||
use crate::ui::dock::{DockMsg, PanelId};
|
||
let mut app = dock_app();
|
||
app.show_browser = false;
|
||
let _ = app.on_dock(DockMsg::EdgeMenu(Some(DockSide::Left)));
|
||
// Checking a closed pallet opens it as a new, shown group here.
|
||
let _ = app.on_dock(DockMsg::EdgeMenuToggle(DockSide::Left, PanelId::Browser));
|
||
assert!(app.show_browser);
|
||
assert_eq!(app.dock.left.len(), 2);
|
||
assert_eq!(app.dock.left[1].panels, vec![PanelId::Browser]);
|
||
assert_eq!(app.dock_shown_group(DockSide::Left), Some(1));
|
||
assert_eq!(app.dock_edge_menu, None, "the menu closes after adding");
|
||
let _ = app.on_dock(DockMsg::EdgeMenu(Some(DockSide::Left)));
|
||
// Unchecking hides it but keeps its place.
|
||
let _ = app.on_dock(DockMsg::EdgeMenuToggle(DockSide::Left, PanelId::Browser));
|
||
assert!(!app.show_browser);
|
||
assert_eq!(
|
||
app.dock.location(PanelId::Browser),
|
||
Some((DockSide::Left, 1))
|
||
);
|
||
// Opening it again from the same edge reuses that place.
|
||
let _ = app.on_dock(DockMsg::EdgeMenuToggle(DockSide::Left, PanelId::Browser));
|
||
assert_eq!(app.dock.left.len(), 2);
|
||
// A pallet open on the other edge is not checked here: picking it
|
||
// moves it over instead of hiding it.
|
||
let _ = app.on_dock(DockMsg::EdgeMenuToggle(
|
||
DockSide::Left,
|
||
PanelId::BlockPalette,
|
||
));
|
||
assert!(app.show_block_palette);
|
||
assert!(app.dock.right.is_empty());
|
||
assert_eq!(
|
||
app.dock.location(PanelId::BlockPalette),
|
||
Some((DockSide::Left, 2))
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn dock_divider_resizes_only_its_group() {
|
||
use crate::app::config::DockSide;
|
||
use crate::ui::dock::{DockGroup, DockMsg, PanelId};
|
||
let mut app = dock_app();
|
||
app.dock.left = vec![
|
||
DockGroup::stack(vec![PanelId::Properties, PanelId::BlockPalette]),
|
||
DockGroup::single(PanelId::Count),
|
||
];
|
||
app.dock.right.clear();
|
||
let _ = app.on_dock(DockMsg::ResizeGrab(PanelId::Properties));
|
||
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(300.0, 100.0)));
|
||
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(330.0, 100.0)));
|
||
let _ = app.on_dock(DockMsg::DragRelease);
|
||
// The group was 260 wide (its widest pallet); it is now 290, while
|
||
// the other group and the pallets' own widths are untouched.
|
||
assert_eq!(app.dock_group_width(DockSide::Left, 0), 290.0);
|
||
assert_eq!(app.dock_group_width(DockSide::Left, 1), 280.0);
|
||
assert_eq!(app.dock.settings(PanelId::Properties).width, 250.0);
|
||
}
|
||
|
||
#[test]
|
||
fn layers_command_opens_a_floating_pallet_and_toggles_it_closed() {
|
||
use crate::ui::dock::PanelId;
|
||
let mut app = dock_app();
|
||
let _ = app.update(Message::ToggleLayers);
|
||
assert!(app.show_layers);
|
||
assert!(app.active_modal.is_none(), "no modal backdrop");
|
||
let f = app
|
||
.dock
|
||
.float_rect(PanelId::Layers)
|
||
.expect("floats on first use");
|
||
assert!(f.x > 0.0 && f.y >= 0.0);
|
||
let _ = app.update(Message::ToggleLayers);
|
||
assert!(!app.show_layers);
|
||
// Its place is kept for next time.
|
||
assert!(app.dock.float_rect(PanelId::Layers).is_some());
|
||
}
|
||
|
||
/// A headless renderer for driving the real view.
|
||
fn headless_renderer() -> iced::Renderer {
|
||
iced_test::futures::futures::executor::block_on(
|
||
<iced::Renderer as iced_test::core::renderer::Headless>::new(
|
||
iced_test::core::renderer::Settings::default(),
|
||
None,
|
||
),
|
||
)
|
||
.expect("headless renderer")
|
||
}
|
||
|
||
/// Size the real view is laid out at.
|
||
const VIEW_SIZE: iced::Size = iced::Size::new(1600.0, 900.0);
|
||
|
||
/// Centre of the first text `label` in the real view.
|
||
fn find_in_view(app: &OpenCADStudio, label: &str) -> iced::Point {
|
||
use iced_test::core::widget;
|
||
use iced_test::runtime::user_interface::{Cache, UserInterface};
|
||
use iced_test::Selector;
|
||
let mut renderer = headless_renderer();
|
||
let mut ui = UserInterface::build(app.view_main(), VIEW_SIZE, Cache::default(), &mut renderer);
|
||
let mut find = Selector::find(label);
|
||
ui.operate(&renderer, &mut widget::operation::black_box(&mut find));
|
||
match widget::Operation::finish(&find) {
|
||
widget::operation::Outcome::Some(Some(target)) => {
|
||
target.visible_bounds().expect("visible").center()
|
||
}
|
||
_ => panic!("{label} not found"),
|
||
}
|
||
}
|
||
|
||
/// Drive the real view through iced's `UserInterface` with the pointer at
|
||
/// `at`: feed `events` one at a time, and between events hand the
|
||
/// published messages to the app and rebuild the view while keeping the
|
||
/// widget-tree cache, as the runtime does.
|
||
fn drive_view(app: &mut OpenCADStudio, at: iced::Point, events: &[iced::Event]) {
|
||
use iced_test::core::{mouse, shell, window};
|
||
use iced_test::runtime::user_interface::{Cache, UserInterface};
|
||
let mut renderer = headless_renderer();
|
||
let mut cache = Cache::default();
|
||
let cursor = mouse::Cursor::Available(at);
|
||
for event in events {
|
||
let mut messages = Vec::new();
|
||
{
|
||
let mut ui = UserInterface::build(app.view_main(), VIEW_SIZE, cache, &mut renderer);
|
||
let _ = ui.update(
|
||
&window::Headless,
|
||
&shell::Waker::noop(),
|
||
std::slice::from_ref(event),
|
||
cursor,
|
||
&mut renderer,
|
||
&mut messages,
|
||
);
|
||
cache = ui.into_cache();
|
||
}
|
||
for message in messages {
|
||
let _ = app.update(message);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// The hover messages an open dropdown inside a hover zone (`zone`) or a
|
||
/// `mouse_area` publishes as the pointer goes from the field onto the
|
||
/// dropdown's list, then out of the area, each tagged with where the pointer
|
||
/// was.
|
||
fn hover_messages_over_an_open_dropdown(zone: bool) -> Vec<(&'static str, &'static str)> {
|
||
use crate::ui::dock::DockMsg;
|
||
use iced::widget::{button, column, container, mouse_area, text};
|
||
use iced_test::core::{mouse, shell, window};
|
||
use iced_test::runtime::user_interface::{Cache, UserInterface};
|
||
let view = || -> iced::Element<'static, Message> {
|
||
let field = button(text("ByLayer")).width(120).height(20);
|
||
// A pointer-wanting list, like a linetype combo's, under the field.
|
||
let list = button(column![text("Continuous"), text("Dashed")])
|
||
.on_press(Message::Dock(DockMsg::HoverEnd(
|
||
crate::ui::dock::PanelId::Layers,
|
||
)))
|
||
.width(120)
|
||
.height(100);
|
||
let dd = iced_aw::DropDown::new(field, list, true)
|
||
.alignment(iced_aw::drop_down::Alignment::Bottom);
|
||
let area = container(dd).width(300).height(400);
|
||
let (enter, exit) = (
|
||
Message::Dock(DockMsg::HoverStay),
|
||
Message::Dock(DockMsg::HoverExit),
|
||
);
|
||
if zone {
|
||
crate::ui::hover_zone::hover_zone(area, enter, exit)
|
||
} else {
|
||
mouse_area(area).on_enter(enter).on_exit(exit).into()
|
||
}
|
||
};
|
||
let mut renderer = headless_renderer();
|
||
let mut cache = Cache::default();
|
||
let mut seen = Vec::new();
|
||
// On the field, on the open list (both inside the area), then outside.
|
||
for (place, x, y) in [
|
||
("field", 60.0, 10.0),
|
||
("list", 60.0, 70.0),
|
||
("list", 60.0, 90.0),
|
||
("outside", 500.0, 70.0),
|
||
] {
|
||
let at = iced::Point::new(x, y);
|
||
let mut messages = Vec::new();
|
||
let mut ui = UserInterface::build(view(), VIEW_SIZE, cache, &mut renderer);
|
||
let _ = ui.update(
|
||
&window::Headless,
|
||
&shell::Waker::noop(),
|
||
&[iced::Event::Mouse(mouse::Event::CursorMoved {
|
||
position: at,
|
||
})],
|
||
mouse::Cursor::Available(at),
|
||
&mut renderer,
|
||
&mut messages,
|
||
);
|
||
cache = ui.into_cache();
|
||
seen.extend(messages.into_iter().filter_map(|m| match m {
|
||
Message::Dock(DockMsg::HoverStay) => Some(("enter", place)),
|
||
Message::Dock(DockMsg::HoverExit) => Some(("exit", place)),
|
||
_ => None,
|
||
}));
|
||
}
|
||
seen
|
||
}
|
||
|
||
#[test]
|
||
fn an_open_dropdown_in_a_hover_zone_does_not_read_as_leaving() {
|
||
with_stack(|| {
|
||
// The bug: a mouse area reads the pointer on the list as gone.
|
||
assert_eq!(
|
||
hover_messages_over_an_open_dropdown(false),
|
||
[("enter", "field"), ("exit", "list")]
|
||
);
|
||
assert_eq!(
|
||
hover_messages_over_an_open_dropdown(true),
|
||
[("enter", "field"), ("exit", "outside")]
|
||
);
|
||
});
|
||
}
|
||
|
||
/// Bounds of the field (text `ByLayer`) and of its open list (text `Opaque`)
|
||
/// for a `drop_down_below` in a scrollable that, like a pallet, sits away
|
||
/// from the window's top-left corner.
|
||
fn drop_down_below_in_an_offset_scrollable() -> (iced::Rectangle, iced::Rectangle) {
|
||
use iced::widget::{button, column, container, row, scrollable, text, Space};
|
||
use iced_test::core::widget;
|
||
use iced_test::runtime::user_interface::{Cache, UserInterface};
|
||
use iced_test::Selector;
|
||
let view = || -> iced::Element<'static, Message> {
|
||
let field = container(text("ByLayer")).width(200).height(20);
|
||
let list = column![
|
||
button(text("Opaque")).width(iced::Fill),
|
||
button(text("50")).width(iced::Fill),
|
||
];
|
||
let dd = crate::ui::color_select::drop_down_below(
|
||
field.into(),
|
||
list.into(),
|
||
None,
|
||
iced::Length::Shrink,
|
||
Message::PropEditChoiceToggle,
|
||
);
|
||
let pallet = scrollable(column![Space::new().height(300), dd]).height(500);
|
||
row![
|
||
Space::new().width(1000),
|
||
container(pallet).width(300).height(iced::Fill)
|
||
]
|
||
.into()
|
||
};
|
||
let mut renderer = headless_renderer();
|
||
let mut ui = UserInterface::build(view(), VIEW_SIZE, Cache::default(), &mut renderer);
|
||
let mut find = |label: &str| {
|
||
let mut find = Selector::find(label);
|
||
ui.operate(&renderer, &mut widget::operation::black_box(&mut find));
|
||
match widget::Operation::finish(&find) {
|
||
widget::operation::Outcome::Some(Some(target)) => target.bounds(),
|
||
_ => panic!("{label} not found"),
|
||
}
|
||
};
|
||
(find("ByLayer"), find("Opaque"))
|
||
}
|
||
|
||
#[test]
|
||
fn a_drop_down_in_a_pallet_opens_right_below_its_field() {
|
||
with_stack(|| {
|
||
let (field, item) = drop_down_below_in_an_offset_scrollable();
|
||
assert!(
|
||
item.y >= field.y + field.height && item.y < field.y + field.height + 20.0,
|
||
"list starts below the field: field {field:?}, first item {item:?}"
|
||
);
|
||
assert!(
|
||
item.x >= field.x - 1.0 && item.x < field.x + field.width,
|
||
"list lines up with the field: field {field:?}, first item {item:?}"
|
||
);
|
||
});
|
||
}
|
||
|
||
#[test]
|
||
fn docked_title_bar_double_click_floats_the_pallet_in_the_real_view() {
|
||
// Building the real view is deeper than a default test thread's stack.
|
||
with_stack(|| {
|
||
use crate::ui::dock::PanelId;
|
||
let mut app = dock_app();
|
||
let press =
|
||
iced::Event::Mouse(iced::mouse::Event::ButtonPressed(iced::mouse::Button::Left));
|
||
let release = iced::Event::Mouse(iced::mouse::Event::ButtonReleased(
|
||
iced::mouse::Button::Left,
|
||
));
|
||
// The block palette's title text sits in its docked title bar.
|
||
let title = find_in_view(&app, "Blocks");
|
||
drive_view(
|
||
&mut app,
|
||
title,
|
||
&[press.clone(), release.clone(), press, release],
|
||
);
|
||
assert!(
|
||
app.dock.float_rect(PanelId::BlockPalette).is_some(),
|
||
"a double-click on a docked title bar floats the pallet"
|
||
);
|
||
});
|
||
}
|
||
|
||
#[test]
|
||
fn hovering_a_strip_icon_reveals_an_auto_hiding_edge_in_the_real_view() {
|
||
// Building the real view is deeper than a default test thread's stack.
|
||
with_stack(|| {
|
||
use crate::app::config::DockSide;
|
||
use crate::ui::dock::{DockMsg, PanelId, STRIP_CELL_H, STRIP_PAD};
|
||
let mut app = dock_app();
|
||
// Mirror the layout onto the left so the block palette's title marks
|
||
// where the workspace starts, then auto-hide the right edge.
|
||
let title = find_in_view(&app, "Blocks");
|
||
let _ = app.on_dock(DockMsg::AutoCollapseToggle(PanelId::BlockPalette));
|
||
assert!(!app.dock_edge_expanded(DockSide::Right));
|
||
// The title bar text sits ~13 px below the workspace top; the strip's
|
||
// first icon is centred STRIP_PAD + half a cell below it.
|
||
let workspace_top = title.y - 13.0;
|
||
let icon = iced::Point::new(
|
||
VIEW_SIZE.width - crate::ui::dock::DOCK_STRIP_W / 2.0,
|
||
workspace_top + STRIP_PAD + STRIP_CELL_H / 2.0,
|
||
);
|
||
drive_view(
|
||
&mut app,
|
||
icon,
|
||
&[iced::Event::Mouse(iced::mouse::Event::CursorMoved {
|
||
position: icon,
|
||
})],
|
||
);
|
||
let _ = app.on_dock(DockMsg::HoverSettled(app.dock_hover_gen));
|
||
assert!(
|
||
app.dock_edge_expanded(DockSide::Right),
|
||
"hovering the icon reveals the auto-hiding edge"
|
||
);
|
||
});
|
||
}
|
||
|
||
#[test]
|
||
fn layers_ribbon_button_opens_the_pallet_without_staying_highlighted() {
|
||
let mut app = dock_app();
|
||
// The ribbon's LAYERS button runs the LAYERS command.
|
||
let click = || Message::RibbonToolClick {
|
||
tool_id: "LAYERS".to_string(),
|
||
event: crate::modules::ModuleEvent::Command("LAYERS".to_string()),
|
||
};
|
||
let _ = app.update(click());
|
||
assert!(app.show_layers);
|
||
assert_eq!(app.ribbon.active_tool(), None);
|
||
// Pressing it again keeps it open, like the Blocks palette's button.
|
||
let _ = app.update(click());
|
||
assert!(app.show_layers);
|
||
}
|
||
|
||
#[test]
|
||
fn dock_double_clicks_float_and_dock_panels() {
|
||
use crate::app::config::DockSide;
|
||
use crate::ui::dock::{DockMsg, PanelId};
|
||
let mut app = dock_app();
|
||
let _ = app.on_dock(DockMsg::FloatOut(PanelId::Properties));
|
||
assert!(app.dock.left.is_empty());
|
||
let f = app.dock.float_rect(PanelId::Properties).expect("floating");
|
||
assert!(f.x > 0.0, "floats clear of the left edge");
|
||
let _ = app.on_dock(DockMsg::DockTo(PanelId::Properties, DockSide::Right));
|
||
assert!(app.dock.floating.is_empty());
|
||
assert_eq!(
|
||
app.dock.location(PanelId::Properties),
|
||
Some((DockSide::Right, 1))
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn blockpalette_width_reset() {
|
||
use crate::app::config::DockSide;
|
||
let mut app = dock_app();
|
||
let id = crate::ui::dock::PanelId::BlockPalette;
|
||
app.dock.set_group_width(DockSide::Right, 0, 500.0);
|
||
let _ = app.on_dock(crate::ui::dock::DockMsg::WidthReset(id));
|
||
assert_eq!(app.dock.group_width(DockSide::Right, 0), 260.0);
|
||
}
|
||
|
||
#[test]
|
||
fn floating_menu_docks_and_allow_docking_keeps_a_drag_floating() {
|
||
use crate::app::config::DockSide;
|
||
use crate::ui::dock::{DockMsg, DropTarget, PanelId};
|
||
let mut app = dock_app();
|
||
let _ = app.on_dock(DockMsg::FloatOut(PanelId::BlockPalette));
|
||
// With Allow docking off, a drag onto the left edge's strip stays
|
||
// floating.
|
||
let _ = app.on_dock(DockMsg::FloatMenu(Some(PanelId::BlockPalette)));
|
||
let _ = app.on_dock(DockMsg::FloatDockingToggle(PanelId::BlockPalette));
|
||
assert_eq!(app.dock_float_menu, None);
|
||
assert!(!app.dock.float_rect(PanelId::BlockPalette).unwrap().docking);
|
||
let _ = app.on_dock(DockMsg::DockGrab(PanelId::BlockPalette));
|
||
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(500.0, 300.0)));
|
||
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(10.0, 60.0)));
|
||
assert!(matches!(drag_target(&app), Some(DropTarget::Float { .. })));
|
||
let _ = app.on_dock(DockMsg::DragRelease);
|
||
let f = app
|
||
.dock
|
||
.float_rect(PanelId::BlockPalette)
|
||
.expect("still floating");
|
||
assert!(!f.docking, "moving it keeps the choice");
|
||
// The menu's Dock left still docks it.
|
||
let _ = app.on_dock(DockMsg::FloatMenu(Some(PanelId::BlockPalette)));
|
||
let _ = app.on_dock(DockMsg::DockTo(PanelId::BlockPalette, DockSide::Left));
|
||
assert_eq!(app.dock_float_menu, None);
|
||
assert_eq!(
|
||
app.dock.location(PanelId::BlockPalette).map(|(s, _)| s),
|
||
Some(DockSide::Left)
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn visibility_ribbon_real_controls_disable_enable_and_open_manager() {
|
||
with_stack(|| {
|
||
use crate::ui::window::block_visibility::Action as A;
|
||
let mut app = authoring_app();
|
||
app.ribbon.set_block_editing(true);
|
||
app.ribbon
|
||
.set_collapse_mode(crate::ui::ribbon::CollapseMode::Compact);
|
||
let i = app.active_tab;
|
||
let before = app.tabs[i].scene.document.clone();
|
||
let point = authoring_control(&app, "BVSTATE_MANAGER");
|
||
authoring_click(&mut app, point);
|
||
assert!(app.block_visibility.is_none());
|
||
assert_eq!(app.tabs[i].scene.document, before);
|
||
authoring_capture(&app, "visibility-ribbon-disabled");
|
||
let _ = app.run_command_line("BPARAMETER VISIBILITY 0.5,0.5");
|
||
let target = app.block_visibility_context().unwrap().target;
|
||
let point = authoring_control(&app, "BVSTATE_MANAGER");
|
||
authoring_click(&mut app, point);
|
||
assert_eq!(
|
||
app.active_modal,
|
||
Some(crate::app::ModalKind::BlockVisibility)
|
||
);
|
||
let _ = app.update(Message::BlockVisibility(A::Name("Alternate".into())));
|
||
let _ = app.update(Message::BlockVisibility(A::New));
|
||
authoring_capture(&app, "visibility-states-dialog");
|
||
let point = find_in_view(&app, "OK");
|
||
authoring_click(&mut app, point);
|
||
assert!(app.block_visibility.is_none());
|
||
assert_eq!(app.block_visibility_context().unwrap().names.len(), 2);
|
||
let messages = {
|
||
let mut simulator = iced_test::Simulator::with_size(
|
||
iced::Settings::default(),
|
||
VIEW_SIZE,
|
||
app.view_main(),
|
||
);
|
||
let target = simulator
|
||
.click(iced::widget::Id::new("BVSTATE_CURRENT"))
|
||
.expect("current-state selector");
|
||
let bounds = target.visible_bounds().unwrap();
|
||
authoring_snapshot(
|
||
&mut simulator,
|
||
&app.active_theme,
|
||
"visibility-state-dropdown",
|
||
);
|
||
// Native pick-list options are painted text, not Text widgets.
|
||
// Pick the second row using the measured selector/row height.
|
||
let position = iced::Point::new(bounds.center_x(), bounds.y + bounds.height * 2.5);
|
||
simulator.point_at(position);
|
||
simulator.simulate([
|
||
iced::Event::Mouse(iced::mouse::Event::CursorMoved { position }),
|
||
iced::Event::Mouse(iced::mouse::Event::ButtonPressed(iced::mouse::Button::Left)),
|
||
iced::Event::Mouse(iced::mouse::Event::ButtonReleased(
|
||
iced::mouse::Button::Left,
|
||
)),
|
||
]);
|
||
simulator.into_messages().collect::<Vec<_>>()
|
||
};
|
||
for message in messages {
|
||
let _ = app.update(message);
|
||
}
|
||
assert_eq!(
|
||
app.block_visibility_context().unwrap().current.as_deref(),
|
||
Some("Alternate")
|
||
);
|
||
let point = authoring_control(&app, "BVMODE_TOGGLE");
|
||
authoring_click(&mut app, point);
|
||
assert!(app.block_visibility_mode);
|
||
authoring_capture(&app, "visibility-ribbon-enabled");
|
||
app.ribbon
|
||
.set_collapse_mode(crate::ui::ribbon::CollapseMode::Collapsed);
|
||
let point = find_in_view(&app, "Visibility ▾");
|
||
authoring_click(&mut app, point);
|
||
let messages = {
|
||
let mut simulator = iced_test::Simulator::with_size(
|
||
iced::Settings::default(),
|
||
VIEW_SIZE,
|
||
app.view_main(),
|
||
);
|
||
simulator
|
||
.click("Visibility States")
|
||
.expect("manager in flyout");
|
||
simulator.into_messages().collect::<Vec<_>>()
|
||
};
|
||
for message in messages {
|
||
let _ = app.update(message);
|
||
}
|
||
assert_eq!(app.block_visibility.as_ref().unwrap().target, target);
|
||
});
|
||
}
|
||
|
||
#[test]
|
||
fn point_parameter_placement_starts_from_the_actual_palette_row() {
|
||
with_stack(|| {
|
||
let mut app = authoring_app();
|
||
let _ = app.dispatch_command("BAUTHORPALETTE");
|
||
let i = app.active_tab;
|
||
let before = app.tabs[i].scene.document.clone();
|
||
let point = authoring_control(&app, "authoring-tool-point");
|
||
authoring_click(&mut app, point);
|
||
assert_eq!(
|
||
app.tabs[i].active_cmd.as_ref().unwrap().name(),
|
||
"BPARAMETER"
|
||
);
|
||
let _ = app.update(Message::CommandInput("0.25,0.25".into()));
|
||
let _ = app.update(Message::CommandSubmit);
|
||
assert_eq!(app.tabs[i].scene.document, before);
|
||
let _ = app.update(Message::CommandInput("0.65,0.15".into()));
|
||
let _ = app.update(Message::CommandSubmit);
|
||
assert!(
|
||
app.tabs[i].scene.preview_wires.is_empty(),
|
||
"placement clears its rubber-band preview"
|
||
);
|
||
let definition = app.tabs[i].active_block_edit_session().unwrap().br_handle;
|
||
assert_eq!(
|
||
crate::scene::block_point_parameter::parameters(
|
||
&app.tabs[i].scene.document,
|
||
definition
|
||
)
|
||
.len(),
|
||
1
|
||
);
|
||
assert!(
|
||
app.visibility_placement_error().is_none(),
|
||
"Visibility can join the Point graph"
|
||
);
|
||
assert_eq!(app.tabs[i].history.undo_stack.len(), 1);
|
||
authoring_capture(&app, "point-parameter-placed");
|
||
let point = authoring_control(&app, "authoring-tool-point");
|
||
authoring_click(&mut app, point);
|
||
let _ = app.update(Message::CommandEscape);
|
||
assert_eq!(app.tabs[i].history.undo_stack.len(), 1);
|
||
});
|
||
}
|
||
|
||
#[test]
|
||
fn distance_parameter_batch_starts_from_actual_palette_rows() {
|
||
with_stack(|| {
|
||
for (id, name) in [
|
||
("authoring-tool-linear", "linear"),
|
||
("authoring-tool-polar", "polar"),
|
||
("authoring-tool-xy", "xy"),
|
||
] {
|
||
let mut app = authoring_app();
|
||
let _ = app.dispatch_command("BAUTHORPALETTE");
|
||
let point = authoring_control(&app, id);
|
||
authoring_click(&mut app, point);
|
||
let i = app.active_tab;
|
||
let before = app.tabs[i].scene.document.clone();
|
||
for text in ["0.25,-0.15", "0.65,0.15"] {
|
||
let _ = app.update(Message::CommandInput(text.into()));
|
||
let _ = app.update(Message::CommandSubmit);
|
||
assert_eq!(app.tabs[i].scene.document, before);
|
||
}
|
||
let _ = app.update(Message::CommandInput("0.3,0.2".into()));
|
||
let _ = app.update(Message::CommandSubmit);
|
||
let definition = app.tabs[i].active_block_edit_session().unwrap().br_handle;
|
||
let found = crate::scene::block_distance_parameter::parameters(
|
||
&app.tabs[i].scene.document,
|
||
definition,
|
||
);
|
||
assert_eq!(found.len(), 1);
|
||
assert!(found[0].kind.name().eq_ignore_ascii_case(name));
|
||
assert_eq!(app.tabs[i].history.undo_stack.len(), 1);
|
||
assert!(app.tabs[i].scene.preview_wires.is_empty());
|
||
let _ = app.run_command_line("ZOOM E");
|
||
authoring_capture(&app, &format!("{name}-parameter-placed"));
|
||
}
|
||
});
|
||
}
|
||
|
||
#[test]
|
||
fn orientation_parameter_batch_starts_from_actual_palette_rows() {
|
||
with_stack(|| {
|
||
for (id, name, inputs) in [
|
||
(
|
||
"authoring-tool-rotation",
|
||
"rotation",
|
||
vec!["0.35,-0.12", "0.65,-0.12", "0.35,0.2"],
|
||
),
|
||
(
|
||
"authoring-tool-alignment",
|
||
"alignment",
|
||
vec!["0.25,-0.1", "0.6,0.15"],
|
||
),
|
||
(
|
||
"authoring-tool-flip",
|
||
"flip",
|
||
vec!["0.35,-0.15", "0.35,0.15", "0.6,0.15"],
|
||
),
|
||
] {
|
||
let mut app = authoring_app();
|
||
let _ = app.dispatch_command("BAUTHORPALETTE");
|
||
let point = authoring_control(&app, id);
|
||
authoring_click(&mut app, point);
|
||
let i = app.active_tab;
|
||
let before = app.tabs[i].scene.document.clone();
|
||
for text in &inputs[..inputs.len() - 1] {
|
||
let _ = app.update(Message::CommandInput((*text).into()));
|
||
let _ = app.update(Message::CommandSubmit);
|
||
assert_eq!(app.tabs[i].scene.document, before);
|
||
}
|
||
let _ = app.update(Message::CommandInput(inputs.last().unwrap().to_string()));
|
||
let _ = app.update(Message::CommandSubmit);
|
||
let definition = app.tabs[i].active_block_edit_session().unwrap().br_handle;
|
||
let found = crate::scene::block_orientation_parameter::parameters(
|
||
&app.tabs[i].scene.document,
|
||
definition,
|
||
);
|
||
assert_eq!(found.len(), 1);
|
||
assert!(found[0].kind.name().eq_ignore_ascii_case(name));
|
||
assert!(app.tabs[i].scene.preview_wires.is_empty());
|
||
assert_eq!(app.tabs[i].history.undo_stack.len(), 1);
|
||
let _ = app.run_command_line("ZOOM E");
|
||
authoring_capture(&app, &format!("{name}-parameter-placed"));
|
||
}
|
||
});
|
||
}
|
||
|
||
fn authoring_app() -> OpenCADStudio {
|
||
use codec::{
|
||
entities::{Insert, Line},
|
||
types::Vector3,
|
||
EntityType,
|
||
};
|
||
let mut app = dock_app();
|
||
let i = app.active_tab;
|
||
app.tabs[i].scene.define_block_raw(
|
||
"AUTHOR",
|
||
Vector3::ZERO,
|
||
vec![EntityType::Line(Line::from_points(
|
||
Vector3::ZERO,
|
||
Vector3::UNIT_X,
|
||
))],
|
||
);
|
||
let reference = app.tabs[i]
|
||
.scene
|
||
.document
|
||
.add_entity(EntityType::Insert(Insert::new("AUTHOR", Vector3::ZERO)))
|
||
.unwrap();
|
||
let _ = app.dispatch_command(&format!("BEDIT_BEGIN:{}", reference.value()));
|
||
app
|
||
}
|
||
|
||
#[test]
|
||
fn authoring_palettes_ribbon_toggle_and_dock_close_leave_document_and_history_unchanged() {
|
||
use crate::ui::dock::{DockMsg, PanelId};
|
||
let mut app = authoring_app();
|
||
let i = app.active_tab;
|
||
let before = app.tabs[i].scene.document.clone();
|
||
let history = app.tabs[i].history.undo_stack.len();
|
||
let click = || Message::RibbonToolClick {
|
||
tool_id: "BAUTHORPALETTE".into(),
|
||
event: crate::modules::ModuleEvent::Command("BAUTHORPALETTE".into()),
|
||
};
|
||
let _ = app.update(click());
|
||
assert!(app.dock_panel_visible(PanelId::AuthoringPalettes));
|
||
assert!(app.ribbon.show_authoring_palettes);
|
||
assert!(app.dock.is_placed(PanelId::AuthoringPalettes));
|
||
let _ = app.update(click());
|
||
assert!(!app.dock_panel_visible(PanelId::AuthoringPalettes));
|
||
assert!(!app.ribbon.show_authoring_palettes);
|
||
let _ = app.dispatch_command("BAUTHORPALETTE");
|
||
let _ = app.on_dock(DockMsg::Close(PanelId::AuthoringPalettes));
|
||
assert!(!app.show_authoring_palettes);
|
||
assert!(!app.ribbon.show_authoring_palettes);
|
||
assert_eq!(app.tabs[i].scene.document, before);
|
||
assert_eq!(app.tabs[i].history.undo_stack.len(), history);
|
||
assert!(!app.block_edit_dirty(i));
|
||
}
|
||
|
||
#[test]
|
||
fn authoring_palettes_follow_editor_context_and_keep_the_docked_placement() {
|
||
use crate::app::config::DockSide;
|
||
use crate::ui::dock::{DockMsg, PanelId};
|
||
let mut app = authoring_app();
|
||
let _ = app.dispatch_command("BAUTHORPALETTE");
|
||
let _ = app.on_dock(DockMsg::DockTo(PanelId::AuthoringPalettes, DockSide::Left));
|
||
let placement = app.dock.location(PanelId::AuthoringPalettes);
|
||
let _ = app.dispatch_command("BTESTBLOCK");
|
||
assert!(!app.dock_panel_visible(PanelId::AuthoringPalettes));
|
||
let _ = app.dispatch_command("BCLOSETESTBLOCK");
|
||
assert!(app.dock_panel_visible(PanelId::AuthoringPalettes));
|
||
let _ = app.dispatch_command("BTESTBLOCK");
|
||
assert!(!app.dock_panel_visible(PanelId::AuthoringPalettes));
|
||
let _ = app.dispatch_command("BAUTHORPALETTECLOSE");
|
||
let _ = app.dispatch_command("BAUTHORPALETTE");
|
||
assert!(!app.show_authoring_palettes);
|
||
assert!(!app.dock_panel_visible(PanelId::AuthoringPalettes));
|
||
let _ = app.dispatch_command("BCLOSETESTBLOCK");
|
||
let _ = app.dispatch_command("BAUTHORPALETTE");
|
||
assert!(app.dock_panel_visible(PanelId::AuthoringPalettes));
|
||
assert_eq!(app.dock.location(PanelId::AuthoringPalettes), placement);
|
||
let _ = app.dispatch_command("BEDIT_DISCARD");
|
||
assert!(!app.dock_panel_visible(PanelId::AuthoringPalettes));
|
||
let _ = app.dispatch_command("BAUTHORPALETTECLOSE");
|
||
let _ = app.dispatch_command("BAUTHORPALETTE");
|
||
assert!(!app.show_authoring_palettes);
|
||
}
|
||
|
||
#[test]
|
||
fn authoring_palettes_toggle_keeps_the_running_drawing_command() {
|
||
let mut app = authoring_app();
|
||
let _ = app.dispatch_command("LINE");
|
||
assert!(app.tabs[app.active_tab].active_cmd.is_some());
|
||
let _ = app.update(Message::RibbonToolClick {
|
||
tool_id: "BAUTHORPALETTE".into(),
|
||
event: crate::modules::ModuleEvent::Command("BAUTHORPALETTE".into()),
|
||
});
|
||
assert!(app.tabs[app.active_tab].active_cmd.is_some());
|
||
let _ = app.dispatch_command("BAUTHORPALETTECLOSE");
|
||
assert!(app.tabs[app.active_tab].active_cmd.is_some());
|
||
}
|
||
|
||
#[test]
|
||
fn authoring_palettes_catalog_in_real_view_has_no_parameter_placement_action() {
|
||
with_stack(|| {
|
||
let mut app = authoring_app();
|
||
let _ = app.dispatch_command("BAUTHORPALETTE");
|
||
let point = find_in_view(&app, "Basepoint");
|
||
let before = app.tabs[app.active_tab].scene.document.clone();
|
||
drive_view(
|
||
&mut app,
|
||
point,
|
||
&[
|
||
iced::Event::Mouse(iced::mouse::Event::ButtonPressed(iced::mouse::Button::Left)),
|
||
iced::Event::Mouse(iced::mouse::Event::ButtonReleased(
|
||
iced::mouse::Button::Left,
|
||
)),
|
||
],
|
||
);
|
||
assert_eq!(app.tabs[app.active_tab].scene.document, before);
|
||
assert!(app.tabs[app.active_tab].active_cmd.is_none());
|
||
});
|
||
}
|
||
|
||
/// Locate rotated category controls by their stable widget IDs.
|
||
fn authoring_control(app: &OpenCADStudio, id: &'static str) -> iced::Point {
|
||
use iced_test::core::widget;
|
||
use iced_test::runtime::user_interface::{Cache, UserInterface};
|
||
use iced_test::Selector;
|
||
let mut renderer = headless_renderer();
|
||
let mut ui = UserInterface::build(app.view_main(), VIEW_SIZE, Cache::default(), &mut renderer);
|
||
let mut find = widget::Id::new(id).find();
|
||
ui.operate(&renderer, &mut widget::operation::black_box(&mut find));
|
||
match widget::Operation::finish(&find) {
|
||
widget::operation::Outcome::Some(Some(target)) => {
|
||
target.visible_bounds().expect("visible").center()
|
||
}
|
||
_ => panic!("{id} not found"),
|
||
}
|
||
}
|
||
|
||
fn authoring_click(app: &mut OpenCADStudio, point: iced::Point) {
|
||
drive_view(
|
||
app,
|
||
point,
|
||
&[
|
||
iced::Event::Mouse(iced::mouse::Event::ButtonPressed(iced::mouse::Button::Left)),
|
||
iced::Event::Mouse(iced::mouse::Event::ButtonReleased(
|
||
iced::mouse::Button::Left,
|
||
)),
|
||
],
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn authoring_palettes_real_tabs_switch_catalogs_and_unavailable_rows_stay_inert() {
|
||
with_stack(|| {
|
||
use crate::ui::window::authoring_palettes::Tab;
|
||
let mut app = authoring_app();
|
||
let _ = app.dispatch_command("BAUTHORPALETTE");
|
||
let i = app.active_tab;
|
||
let before = app.tabs[i].scene.document.clone();
|
||
let history = app.tabs[i].history.undo_stack.len();
|
||
let _ = app.dispatch_command("LINE");
|
||
for (tab, row) in [
|
||
(Tab::Actions, "Block Properties Table"),
|
||
(Tab::ParameterSets, "Point Move"),
|
||
(Tab::Parameters, "Basepoint"),
|
||
] {
|
||
let point = authoring_control(&app, tab.widget_id());
|
||
authoring_click(&mut app, point);
|
||
assert_eq!(app.authoring_palette_tab, tab);
|
||
authoring_capture(&app, tab.widget_id());
|
||
let point = find_in_view(&app, row);
|
||
authoring_click(&mut app, point);
|
||
if tab == Tab::ParameterSets {
|
||
let mut simulator = iced_test::Simulator::with_size(
|
||
iced::Settings::default(),
|
||
VIEW_SIZE,
|
||
app.view_main(),
|
||
);
|
||
simulator.point_at(point);
|
||
simulator.simulate([iced::Event::Mouse(iced::mouse::Event::WheelScrolled {
|
||
delta: iced::mouse::ScrollDelta::Lines { x: 0.0, y: -100.0 },
|
||
})]);
|
||
simulator
|
||
.click("Lookup Set")
|
||
.expect("last set is reachable by scrolling");
|
||
authoring_snapshot(&mut simulator, &app.active_theme, "parameter-sets-bottom");
|
||
let messages = simulator.into_messages().collect::<Vec<_>>();
|
||
for message in messages {
|
||
let _ = app.update(message);
|
||
}
|
||
}
|
||
assert_eq!(
|
||
app.tabs[i]
|
||
.active_cmd
|
||
.as_ref()
|
||
.map(|command| command.name()),
|
||
Some("LINE")
|
||
);
|
||
}
|
||
assert_eq!(app.tabs[i].scene.document, before);
|
||
assert_eq!(app.tabs[i].history.undo_stack.len(), history);
|
||
assert!(!app.block_edit_dirty(i));
|
||
});
|
||
}
|
||
|
||
#[test]
|
||
fn authoring_palettes_constraints_start_the_existing_commands_from_real_rows() {
|
||
with_stack(|| {
|
||
use crate::ui::window::authoring_palettes::Tab;
|
||
let mut app = authoring_app();
|
||
let _ = app.dispatch_command("BAUTHORPALETTE");
|
||
let point = authoring_control(&app, Tab::Constraints.widget_id());
|
||
authoring_click(&mut app, point);
|
||
assert_eq!(app.authoring_palette_tab, Tab::Constraints);
|
||
authoring_capture(&app, Tab::Constraints.widget_id());
|
||
// The palette row is distinct from the ribbon's Coincident button.
|
||
let point = authoring_control(&app, "authoring-tool-CCONSTRAINT");
|
||
authoring_click(&mut app, point);
|
||
assert_eq!(
|
||
app.tabs[app.active_tab]
|
||
.active_cmd
|
||
.as_ref()
|
||
.map(|command| command.name()),
|
||
Some("GCCOINCIDENT")
|
||
);
|
||
let messages = {
|
||
let mut simulator = iced_test::Simulator::with_size(
|
||
iced::Settings::default(),
|
||
VIEW_SIZE,
|
||
app.view_main(),
|
||
);
|
||
simulator.point_at(point);
|
||
simulator.simulate([iced::Event::Mouse(iced::mouse::Event::WheelScrolled {
|
||
delta: iced::mouse::ScrollDelta::Lines { x: 0.0, y: -100.0 },
|
||
})]);
|
||
simulator
|
||
.click(iced_test::core::widget::Id::new(
|
||
"authoring-tool-DCDIAMETER",
|
||
))
|
||
.expect("last dimensional constraint is reachable by scrolling");
|
||
authoring_snapshot(&mut simulator, &app.active_theme, "constraints-bottom");
|
||
simulator.into_messages().collect::<Vec<_>>()
|
||
};
|
||
for message in messages {
|
||
let _ = app.update(message);
|
||
}
|
||
assert_eq!(
|
||
app.tabs[app.active_tab]
|
||
.active_cmd
|
||
.as_ref()
|
||
.map(|command| command.name()),
|
||
Some("DCDIAMETER")
|
||
);
|
||
assert!(!app.block_edit_dirty(app.active_tab));
|
||
});
|
||
}
|
||
|
||
#[test]
|
||
fn authoring_palettes_selected_page_survives_test_block_and_close_and_ignores_hidden_input() {
|
||
use crate::ui::window::authoring_palettes::Tab;
|
||
let mut app = authoring_app();
|
||
let _ = app.dispatch_command("BAUTHORPALETTE");
|
||
let _ = app.update(Message::AuthoringPaletteTabSelected(Tab::ParameterSets));
|
||
let _ = app.dispatch_command("BTESTBLOCK");
|
||
let _ = app.update(Message::AuthoringPaletteTabSelected(Tab::Constraints));
|
||
assert_eq!(app.authoring_palette_tab, Tab::ParameterSets);
|
||
let _ = app.dispatch_command("BCLOSETESTBLOCK");
|
||
assert_eq!(app.authoring_palette_tab, Tab::ParameterSets);
|
||
let _ = app.dispatch_command("BAUTHORPALETTECLOSE");
|
||
let _ = app.update(Message::AuthoringPaletteTabSelected(Tab::Actions));
|
||
assert_eq!(app.authoring_palette_tab, Tab::ParameterSets);
|
||
let _ = app.dispatch_command("BAUTHORPALETTE");
|
||
assert_eq!(app.authoring_palette_tab, Tab::ParameterSets);
|
||
}
|
||
|
||
/// Optional rendered evidence when reviewing palette layout changes.
|
||
fn authoring_capture(app: &OpenCADStudio, name: &str) {
|
||
if std::env::var_os("OCS_AUTHORING_PALETTE_CAPTURES").is_none() {
|
||
return;
|
||
}
|
||
let mut simulator =
|
||
iced_test::Simulator::with_size(iced::Settings::default(), VIEW_SIZE, app.view_main());
|
||
authoring_snapshot(&mut simulator, &app.active_theme, name);
|
||
}
|
||
|
||
fn authoring_snapshot(
|
||
simulator: &mut iced_test::Simulator<'_, Message>,
|
||
theme: &iced::Theme,
|
||
name: &str,
|
||
) {
|
||
let Some(directory) = std::env::var_os("OCS_AUTHORING_PALETTE_CAPTURES") else {
|
||
return;
|
||
};
|
||
let path = std::path::PathBuf::from(directory).join(format!("{name}.png"));
|
||
assert!(simulator
|
||
.snapshot(theme)
|
||
.expect("render palette")
|
||
.matches_image(path)
|
||
.expect("write palette evidence"));
|
||
}
|
||
|
||
#[test]
|
||
fn visibility_parameter_placement_starts_from_the_actual_palette_row() {
|
||
with_stack(|| {
|
||
let mut app = authoring_app();
|
||
let _ = app.dispatch_command("BAUTHORPALETTE");
|
||
let before = app.tabs[app.active_tab].scene.document.clone();
|
||
let point = authoring_control(&app, "authoring-tool-visibility");
|
||
authoring_click(&mut app, point);
|
||
assert_eq!(
|
||
app.tabs[app.active_tab].active_cmd.as_ref().unwrap().name(),
|
||
"BPARAMETER"
|
||
);
|
||
assert_eq!(app.tabs[app.active_tab].scene.document, before);
|
||
let _ = app.update(Message::CommandInput("0.25,0.25,0".into()));
|
||
let _ = app.update(Message::CommandSubmit);
|
||
assert!(app
|
||
.visibility_placement_error()
|
||
.unwrap()
|
||
.contains("already"));
|
||
let before = app.tabs[app.active_tab].scene.document.clone();
|
||
let _ = app.dispatch_command("LINE");
|
||
let point = authoring_control(&app, "authoring-tool-visibility");
|
||
authoring_click(&mut app, point);
|
||
assert_eq!(
|
||
app.tabs[app.active_tab].active_cmd.as_ref().unwrap().name(),
|
||
"LINE"
|
||
);
|
||
assert_eq!(app.tabs[app.active_tab].scene.document, before);
|
||
authoring_capture(&app, "visibility-parameter-placed");
|
||
});
|
||
}
|