forked from 1b-agent/opencadstudio-upstream
feat(measure): add radius and angle tools with dynamic measurement overlay
(cherry picked from commit aac44906a3)
This commit is contained in:
1 parent
843cbcd607
commit
3c55e6d7e1
42 files changed
+1624
-53
No files matched your search
@@ -1163,6 +1163,7 @@ impl OpenCADStudio {
|
||||
|
||||
fn handle_cancel(&mut self, cancel: CmdResult) {
|
||||
let i = self.active_tab;
|
||||
self.tabs[i].measurement.clear();
|
||||
let space_changed = matches!(cancel, CmdResult::CancelForSpaceChange);
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
if !space_changed && self.tabs[i].suspended_cmd.is_some() {
|
||||
@@ -1304,6 +1305,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
|
||||
fn handle_measurement(&mut self, msg: String) {
|
||||
self.remember_measurement(&msg);
|
||||
let i = self.active_tab;
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
@@ -1315,6 +1317,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
|
||||
fn handle_report_measurement(&mut self, msg: String) {
|
||||
self.remember_measurement(&msg);
|
||||
let i = self.active_tab;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
@@ -1336,6 +1339,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
|
||||
fn handle_report_measurement_and_deselect(&mut self, msg: String) {
|
||||
self.remember_measurement(&msg);
|
||||
let i = self.active_tab;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.deselect_all();
|
||||
|
||||
@@ -598,9 +598,13 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
|
||||
"MEASUREGEOM" => {
|
||||
"MEASUREGEOM" | "MEASURERADIUS" | "MEASUREANGLE" => {
|
||||
use crate::modules::draw::inquiry::measuregeom::MeasureGeomCommand;
|
||||
let new_cmd = MeasureGeomCommand::new();
|
||||
let new_cmd = match cmd {
|
||||
"MEASURERADIUS" => MeasureGeomCommand::radius(),
|
||||
"MEASUREANGLE" => MeasureGeomCommand::angle(),
|
||||
_ => MeasureGeomCommand::new(),
|
||||
};
|
||||
self.command_line.push_info(&new_cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||
}
|
||||
|
||||
@@ -64,6 +64,7 @@ impl OpenCADStudio {
|
||||
/// (the block palette), so they don't inherit the previous command's last
|
||||
/// point, snaps or dynamic-input values. (#1525)
|
||||
pub(in crate::app) fn reset_command_start_state(&mut self, i: usize) {
|
||||
self.tabs[i].measurement.clear();
|
||||
// A command parked behind a transparent zoom / MTP goes with it.
|
||||
self.tabs[i].suspended_cmd = None;
|
||||
self.tabs[i].transparent_resume = false;
|
||||
|
||||
+11
-1
@@ -308,7 +308,17 @@ fn command_category(name: &str) -> &'static str {
|
||||
.contains(&name)
|
||||
{
|
||||
"modify"
|
||||
} else if ["DIST", "ID", "AREA", "MEASURE", "MEASUREGEOM"].contains(&name) {
|
||||
} else if [
|
||||
"DIST",
|
||||
"ID",
|
||||
"AREA",
|
||||
"MEASURE",
|
||||
"MEASUREGEOM",
|
||||
"MEASURERADIUS",
|
||||
"MEASUREANGLE",
|
||||
]
|
||||
.contains(&name)
|
||||
{
|
||||
"inspect"
|
||||
} else if ["ZOOM", "PAN", "UCS", "PLAN", "VPORTS", "3DORBIT"].contains(&name) {
|
||||
"view"
|
||||
|
||||
@@ -157,6 +157,7 @@ pub(super) struct DocumentTab {
|
||||
pub(super) properties: PropertiesPanel,
|
||||
pub(super) layers: LayerPanel,
|
||||
pub(super) active_cmd: Option<Box<dyn CadCommand>>,
|
||||
pub(super) measurement: super::measurement::MeasurementDisplay,
|
||||
/// Remaining command tokens queued behind a PAUSE (or `\`) awaiting user interaction.
|
||||
pub(super) pending_pause_tokens: Option<Vec<String>>,
|
||||
/// The selection set the most recent command worked on, captured when a
|
||||
@@ -644,6 +645,7 @@ impl DocumentTab {
|
||||
properties: PropertiesPanel::empty(),
|
||||
layers: LayerPanel::default(),
|
||||
active_cmd: None,
|
||||
measurement: super::measurement::MeasurementDisplay::default(),
|
||||
pending_pause_tokens: None,
|
||||
last_cmd: None,
|
||||
last_draw_anchor: None,
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
//! Per-drawing, transient inquiry UI; never serialized into document entities.
|
||||
use super::OpenCADStudio;
|
||||
use iced::Point;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(super) struct MeasurementReadout {
|
||||
pub text: String,
|
||||
pub anchor: Point,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct MeasurementDisplay {
|
||||
pub result: Option<MeasurementReadout>,
|
||||
pub live_visible: bool,
|
||||
}
|
||||
|
||||
impl MeasurementDisplay {
|
||||
pub fn clear(&mut self) {
|
||||
*self = Self::default();
|
||||
}
|
||||
|
||||
/// Called on press before ordinary input processing. It cannot capture
|
||||
/// the event, so this same press can also pick/select/pan/finish a command.
|
||||
pub fn dismiss_result(&mut self) {
|
||||
self.result = None;
|
||||
}
|
||||
}
|
||||
|
||||
impl OpenCADStudio {
|
||||
pub(super) fn remember_measurement(&mut self, text: &str) {
|
||||
let tab = &mut self.tabs[self.active_tab];
|
||||
if tab
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.is_some_and(|c| c.has_measurement_readout())
|
||||
{
|
||||
tab.measurement.result = Some(MeasurementReadout {
|
||||
text: text.to_owned(),
|
||||
anchor: tab.last_cursor_screen,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn measurement_readout(&self, index: usize) -> Option<MeasurementReadout> {
|
||||
let tab = &self.tabs[index];
|
||||
if let Some(result) = &tab.measurement.result {
|
||||
return Some(result.clone());
|
||||
}
|
||||
if !tab.measurement.live_visible {
|
||||
return None;
|
||||
}
|
||||
let text = tab
|
||||
.active_cmd
|
||||
.as_ref()?
|
||||
.measurement_readout(tab.last_cursor_world)?;
|
||||
Some(MeasurementReadout {
|
||||
text,
|
||||
anchor: tab.last_cursor_screen,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod measurement_tests {
|
||||
use super::*;
|
||||
use crate::app::Message;
|
||||
use crate::command::{all_registered_command_names, StepInput};
|
||||
use crate::modules::{CadModule, ModuleEvent, RibbonItem};
|
||||
use glam::DVec3;
|
||||
|
||||
fn app() -> OpenCADStudio {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
app.automation_op(r#"{"op":"new"}"#);
|
||||
let i = app.active_tab;
|
||||
app.tabs[i].scene.selection.borrow_mut().view.vp_size = (800.0, 600.0);
|
||||
app.snapper.snap_enabled = false;
|
||||
app.snapper.grid_snap_on = false;
|
||||
app.snapper.otrack_enabled = false;
|
||||
app.ortho_mode = false;
|
||||
app.polar_mode = false;
|
||||
app.dyn_input = false;
|
||||
app
|
||||
}
|
||||
fn finish_distance(app: &mut OpenCADStudio) {
|
||||
let _ = app.dispatch_command("DIST");
|
||||
let _ = app.feed_command(StepInput::Point(DVec3::ZERO));
|
||||
let _ = app.feed_command(StepInput::Point(DVec3::new(3.0, 4.0, 0.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn measurement_menu_registry_and_dispatch() {
|
||||
let names = all_registered_command_names();
|
||||
let module = crate::modules::draw::DrawModule;
|
||||
let menu = module
|
||||
.ribbon_groups()
|
||||
.iter()
|
||||
.flat_map(|g| &g.tools)
|
||||
.find(|item| {
|
||||
matches!(
|
||||
item,
|
||||
RibbonItem::LargeDropdown {
|
||||
id: "MEASURE_MENU",
|
||||
..
|
||||
}
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
let RibbonItem::LargeDropdown { items, default, .. } = menu else {
|
||||
unreachable!()
|
||||
};
|
||||
assert_eq!(*default, "DIST");
|
||||
assert_eq!(
|
||||
items.iter().map(|item| item.0).collect::<Vec<_>>(),
|
||||
["DIST", "MEASURERADIUS", "MEASUREANGLE", "AREA"]
|
||||
);
|
||||
let mut app = app();
|
||||
for name in [
|
||||
"DIST",
|
||||
"MEASURERADIUS",
|
||||
"MEASUREANGLE",
|
||||
"AREA",
|
||||
"MEASUREGEOM",
|
||||
] {
|
||||
assert!(names.contains(&name));
|
||||
let _ = app.dispatch_command(name);
|
||||
assert_eq!(
|
||||
app.tabs[app.active_tab].active_cmd.as_ref().unwrap().name(),
|
||||
name
|
||||
);
|
||||
let _ = app.update(Message::RibbonToolClick {
|
||||
tool_id: name.into(),
|
||||
event: ModuleEvent::Command(name.into()),
|
||||
});
|
||||
assert_eq!(
|
||||
app.tabs[app.active_tab].active_cmd.as_ref().unwrap().name(),
|
||||
name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn measurement_result_persists_without_document_changes() {
|
||||
let mut app = app();
|
||||
let i = app.active_tab;
|
||||
let count = app.tabs[i].scene.document.entity_count();
|
||||
let dirty = app.tabs[i].dirty;
|
||||
let revision = app.tabs[i].edit_revision;
|
||||
let _ = app.update(Message::ViewportMove(Point::new(350.0, 250.0)));
|
||||
finish_distance(&mut app);
|
||||
assert!(app.tabs[i].active_cmd.is_none());
|
||||
let result = app.measurement_readout(i).unwrap();
|
||||
assert!(app.command_line.history_plain_text().contains(&result.text));
|
||||
let _ = app.update(Message::ViewportLeftRelease);
|
||||
let _ = app.update(Message::ViewportMove(Point::new(450.0, 300.0)));
|
||||
let retained = app.measurement_readout(i).unwrap();
|
||||
assert_eq!(result.text, retained.text);
|
||||
assert_eq!(result.anchor, retained.anchor);
|
||||
assert_eq!(app.tabs[i].scene.document.entity_count(), count);
|
||||
assert_eq!(app.tabs[i].dirty, dirty);
|
||||
assert_eq!(app.tabs[i].edit_revision, revision);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn measurement_dismiss_click_also_starts_selection() {
|
||||
let mut app = app();
|
||||
let i = app.active_tab;
|
||||
finish_distance(&mut app);
|
||||
let _ = app.update(Message::ViewportMove(Point::new(350.0, 250.0)));
|
||||
let _ = app.update(Message::ViewportLeftPress);
|
||||
assert!(app.measurement_readout(i).is_none());
|
||||
assert!(app.tabs[i].scene.selection.borrow().input.left_down);
|
||||
let _ = app.update(Message::ViewportLeftRelease);
|
||||
assert!(app.tabs[i]
|
||||
.scene
|
||||
.selection
|
||||
.borrow()
|
||||
.gesture
|
||||
.box_anchor
|
||||
.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn measurement_final_pick_survives_its_own_release() {
|
||||
let mut app = app();
|
||||
let i = app.active_tab;
|
||||
let _ = app.dispatch_command("DIST");
|
||||
for point in [Point::new(350.0, 250.0), Point::new(430.0, 280.0)] {
|
||||
let _ = app.update(Message::ViewportMove(point));
|
||||
let _ = app.update(Message::ViewportLeftPress);
|
||||
let _ = app.update(Message::ViewportLeftRelease);
|
||||
}
|
||||
assert!(app.tabs[i].active_cmd.is_none());
|
||||
assert!(app.measurement_readout(i).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn measurement_live_without_dyn_and_escape() {
|
||||
let mut app = app();
|
||||
let i = app.active_tab;
|
||||
let _ = app.dispatch_command("DIST");
|
||||
let _ = app.feed_command(StepInput::Point(DVec3::ZERO));
|
||||
let _ = app.update(Message::ViewportMove(Point::new(410.0, 310.0)));
|
||||
let first = app.measurement_readout(i).unwrap().text;
|
||||
let _ = app.update(Message::ViewportMove(Point::new(510.0, 310.0)));
|
||||
assert_ne!(first, app.measurement_readout(i).unwrap().text);
|
||||
let _ = app.update(Message::CommandEscape);
|
||||
assert!(app.measurement_readout(i).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn measurement_escape_new_command_and_mouse_buttons_clear_result() {
|
||||
let mut app = app();
|
||||
let i = app.active_tab;
|
||||
for event in [
|
||||
Message::CommandEscape,
|
||||
Message::ViewportRightPress,
|
||||
Message::ViewportMiddlePress,
|
||||
] {
|
||||
finish_distance(&mut app);
|
||||
assert!(app.measurement_readout(i).is_some());
|
||||
let _ = app.update(event);
|
||||
assert!(app.measurement_readout(i).is_none());
|
||||
}
|
||||
finish_distance(&mut app);
|
||||
let _ = app.dispatch_command("MEASURERADIUS");
|
||||
assert!(app.measurement_readout(i).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn measurement_area_add_dismiss_click_is_also_next_corner() {
|
||||
let mut app = app();
|
||||
let i = app.active_tab;
|
||||
let _ = app.dispatch_command("AREA");
|
||||
let _ = app.feed_command(StepInput::Text("ADD".into()));
|
||||
for p in [DVec3::ZERO, DVec3::X * 4.0, DVec3::Y * 3.0] {
|
||||
let _ = app.feed_command(StepInput::Point(p));
|
||||
}
|
||||
let _ = app.feed_command(StepInput::Enter);
|
||||
assert_eq!(app.tabs[i].active_cmd.as_ref().unwrap().name(), "AREA");
|
||||
assert!(app.measurement_readout(i).is_some());
|
||||
for p in [Point::new(350.0, 250.0), Point::new(410.0, 250.0)] {
|
||||
let _ = app.update(Message::ViewportMove(p));
|
||||
let _ = app.update(Message::ViewportLeftPress);
|
||||
let _ = app.update(Message::ViewportLeftRelease);
|
||||
}
|
||||
assert!(app.tabs[i].measurement.result.is_none());
|
||||
assert!(
|
||||
app.measurement_readout(i).is_some(),
|
||||
"both corners must reach AREA"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn measurement_radius_and_lines_real_entity_injection() {
|
||||
use codec::{
|
||||
entities::{Circle, Line},
|
||||
types::Vector3,
|
||||
EntityType,
|
||||
};
|
||||
let mut app = app();
|
||||
let i = app.active_tab;
|
||||
let circle = app.tabs[i]
|
||||
.scene
|
||||
.add_entity(EntityType::Circle(Circle::from_coords(0.0, 0.0, 0.0, 2.0)));
|
||||
let first = app.tabs[i]
|
||||
.scene
|
||||
.add_entity(EntityType::Line(Line::from_points(
|
||||
Vector3::ZERO,
|
||||
Vector3::UNIT_X,
|
||||
)));
|
||||
let second = app.tabs[i]
|
||||
.scene
|
||||
.add_entity(EntityType::Line(Line::from_points(
|
||||
Vector3::ZERO,
|
||||
Vector3::UNIT_Y,
|
||||
)));
|
||||
let _ = app.dispatch_command("MEASURERADIUS");
|
||||
let _ = app.feed_command(StepInput::EntityPick(circle, DVec3::X * 2.0));
|
||||
assert!(app.tabs[i].active_cmd.is_none());
|
||||
assert!(app.measurement_readout(i).unwrap().text.contains("Radius"));
|
||||
let _ = app.dispatch_command("MEASUREANGLE");
|
||||
let _ = app.feed_command(StepInput::Text("LINES".into()));
|
||||
let _ = app.feed_command(StepInput::EntityPick(first, DVec3::X));
|
||||
let _ = app.feed_command(StepInput::EntityPick(second, DVec3::Y));
|
||||
assert!(app.tabs[i].active_cmd.is_none());
|
||||
assert!(app.measurement_readout(i).unwrap().text.contains("90"));
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@ mod find_replace;
|
||||
pub(crate) mod helpers;
|
||||
mod history;
|
||||
mod layers;
|
||||
mod measurement;
|
||||
mod model_ops;
|
||||
mod mtext_editor;
|
||||
mod navigation;
|
||||
|
||||
@@ -1249,6 +1249,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
|
||||
pub(super) fn on_command_escape(&mut self) -> Task<Message> {
|
||||
self.tabs[self.active_tab].measurement.clear();
|
||||
// Escape answers a pending client getpoint request first: no point.
|
||||
if self.control.get_point.is_some() {
|
||||
self.resolve_get_point(None);
|
||||
|
||||
@@ -4564,6 +4564,7 @@ impl OpenCADStudio {
|
||||
Message::ViewportRightPress => {
|
||||
self.graphic_attributes.open_menu = None;
|
||||
let i = self.active_tab;
|
||||
self.tabs[i].measurement.dismiss_result();
|
||||
self.ribbon.close_dropdown();
|
||||
// Shift+RMB: the one-shot snap override menu at the cursor —
|
||||
// pick a snap for just the next point, then it expires (#337).
|
||||
|
||||
@@ -1236,6 +1236,7 @@ impl OpenCADStudio {
|
||||
return Task::none();
|
||||
}
|
||||
let i = self.active_tab;
|
||||
self.tabs[i].measurement.live_visible = true;
|
||||
if let Some(task) = self.start_drag_move(i, p) {
|
||||
return Task::batch([task, self.on_viewport_move(p)]);
|
||||
}
|
||||
@@ -3162,6 +3163,7 @@ impl OpenCADStudio {
|
||||
|
||||
pub(crate) fn on_viewport_exit(&mut self) -> Task<Message> {
|
||||
let i = self.active_tab;
|
||||
self.tabs[i].measurement.live_visible = false;
|
||||
self.hover_dwell = None;
|
||||
if self.tabs[i]
|
||||
.active_cmd
|
||||
@@ -3658,6 +3660,7 @@ impl OpenCADStudio {
|
||||
|
||||
pub(super) fn on_viewport_left_press(&mut self) -> Task<Message> {
|
||||
let i = self.active_tab;
|
||||
self.tabs[i].measurement.dismiss_result();
|
||||
self.block_palette.pressed = None;
|
||||
if let Some(command) = self.tabs[i].active_cmd.as_mut() {
|
||||
command.set_ctrl(self.ctrl_down);
|
||||
@@ -5916,6 +5919,7 @@ properties={:.1}ms picked={}",
|
||||
|
||||
pub(super) fn on_viewport_middle_press(&mut self) -> Task<Message> {
|
||||
let i = self.active_tab;
|
||||
self.tabs[i].measurement.dismiss_result();
|
||||
self.clear_navigation_hover(i);
|
||||
self.ribbon.close_dropdown();
|
||||
self.tabs[i].scene.remember_current_view();
|
||||
|
||||
@@ -1809,6 +1809,15 @@ bg={bg_ms:.1}ms n={view_count}"
|
||||
} else {
|
||||
34.0 + self.command_line.overlay_lines_height()
|
||||
};
|
||||
if !tab.is_start {
|
||||
if let Some(readout) = self.measurement_readout(self.active_tab) {
|
||||
viewport_stack = viewport_stack.push(overlay::measurement_overlay(
|
||||
readout.anchor,
|
||||
command_line_inset,
|
||||
readout.text,
|
||||
));
|
||||
}
|
||||
}
|
||||
// Quick Properties: stay near the selection cursor, flipping around
|
||||
// it as needed to remain inside the visible drawing area.
|
||||
if self.quick_properties && !tab.is_start {
|
||||
|
||||
+119
-1
@@ -928,6 +928,7 @@ fn position_canvas_overlay_clamped<'a>(
|
||||
bottom_inset: bottom_inset.max(0.0),
|
||||
gap: 0.0,
|
||||
offset,
|
||||
passive: false,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -944,6 +945,48 @@ pub(super) fn position_canvas_overlay_near_cursor<'a>(
|
||||
bottom_inset: bottom_inset.max(0.0),
|
||||
gap: 12.0,
|
||||
offset: Vector::ZERO,
|
||||
passive: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Measured, wrapping text in the existing clamped panel layout. Input goes
|
||||
/// straight through, including clicks directly on the result text.
|
||||
pub(super) fn measurement_overlay<'a>(
|
||||
anchor: iced::Point,
|
||||
bottom_inset: f32,
|
||||
value: String,
|
||||
) -> Element<'a, Message> {
|
||||
// Inquiry commands return a compact, comma-separated summary for the
|
||||
// command line. Show its fields on separate lines in the transient HUD,
|
||||
// without altering the command-line history or the measured precision.
|
||||
let display_value = if value.contains("\n") {
|
||||
value.replace(", ", "\n").replace(", ", "\n")
|
||||
} else {
|
||||
value
|
||||
};
|
||||
let panel = container(text(display_value).size(12))
|
||||
.padding([6, 8])
|
||||
.width(Length::Fit.max(300.0))
|
||||
.style(|theme: &Theme| {
|
||||
let palette = theme.palette();
|
||||
container::Style {
|
||||
background: Some(Background::Color(palette.background.weak.color)),
|
||||
text_color: Some(palette.background.weak.text),
|
||||
border: Border {
|
||||
color: palette.background.neutral.color,
|
||||
width: 1.0,
|
||||
radius: 3.0.into(),
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
});
|
||||
Element::new(ClampedPin {
|
||||
content: panel.into(),
|
||||
anchor,
|
||||
bottom_inset: bottom_inset.max(0.0),
|
||||
gap: 18.0,
|
||||
offset: Vector::ZERO,
|
||||
passive: true,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -954,6 +997,8 @@ struct ClampedPin<'a> {
|
||||
gap: f32,
|
||||
/// Preferred placement shift from the anchor (applied before fitting).
|
||||
offset: Vector,
|
||||
/// Readouts prefer above the cursor and never participate in input.
|
||||
passive: bool,
|
||||
}
|
||||
|
||||
impl Widget<Message, Theme, iced::Renderer> for ClampedPin<'_> {
|
||||
@@ -1008,7 +1053,9 @@ impl Widget<Message, Theme, iced::Renderer> for ClampedPin<'_> {
|
||||
let max_y = (max.height - bottom_inset - MARGIN - content.height).max(MARGIN);
|
||||
let below = self.anchor.y + self.gap + self.offset.y;
|
||||
let above = self.anchor.y - self.gap - content.height;
|
||||
let y = if below <= max_y {
|
||||
let y = if self.passive && above >= MARGIN && above <= max_y {
|
||||
above
|
||||
} else if below <= max_y {
|
||||
below.max(MARGIN)
|
||||
} else if above >= MARGIN {
|
||||
above
|
||||
@@ -1016,6 +1063,9 @@ impl Widget<Message, Theme, iced::Renderer> for ClampedPin<'_> {
|
||||
max_y
|
||||
};
|
||||
|
||||
// A cursor over the command-line inset can put even the "above"
|
||||
// candidate below the safe area. Keep this passive readout inside it.
|
||||
let y = if self.passive { y.min(max_y) } else { y };
|
||||
node = node.move_to(iced::Point::new(x, y));
|
||||
let size = limits.resolve(Length::Fill, Length::Fill, node.size());
|
||||
layout::Node::with_children(size, vec![node])
|
||||
@@ -1046,6 +1096,9 @@ impl Widget<Message, Theme, iced::Renderer> for ClampedPin<'_> {
|
||||
shell: &mut Shell<'_, Message>,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
if self.passive {
|
||||
return;
|
||||
}
|
||||
self.content.as_widget_mut().update(
|
||||
tree,
|
||||
event,
|
||||
@@ -1065,6 +1118,9 @@ impl Widget<Message, Theme, iced::Renderer> for ClampedPin<'_> {
|
||||
viewport: &Rectangle,
|
||||
renderer: &iced::Renderer,
|
||||
) -> mouse::Interaction {
|
||||
if self.passive {
|
||||
return mouse::Interaction::None;
|
||||
}
|
||||
self.content.as_widget().mouse_interaction(
|
||||
tree,
|
||||
layout.children().next().unwrap(),
|
||||
@@ -1872,3 +1928,65 @@ fn qselect_content<'a>(
|
||||
|
||||
panel.into()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod measurement_tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn measurement_overlay_passes_all_clicks_on_text() {
|
||||
let underlay = mouse_area(Space::new().width(Fill).height(Fill))
|
||||
.on_press(Message::ViewportLeftPress)
|
||||
.on_right_press(Message::ViewportRightPress)
|
||||
.on_middle_press(Message::ViewportMiddlePress);
|
||||
let mut ui = iced_test::simulator(stack![
|
||||
underlay,
|
||||
measurement_overlay(
|
||||
iced::Point::new(400.0, 300.0),
|
||||
60.0,
|
||||
"measurement-result".into()
|
||||
)
|
||||
]);
|
||||
let center = ui
|
||||
.find("measurement-result")
|
||||
.unwrap()
|
||||
.visible_bounds()
|
||||
.unwrap()
|
||||
.center();
|
||||
ui.point_at(center);
|
||||
for button in [
|
||||
iced::mouse::Button::Left,
|
||||
iced::mouse::Button::Right,
|
||||
iced::mouse::Button::Middle,
|
||||
] {
|
||||
ui.simulate([Event::Mouse(iced::mouse::Event::ButtonPressed(button))]);
|
||||
ui.simulate([Event::Mouse(iced::mouse::Event::ButtonReleased(button))]);
|
||||
}
|
||||
let messages: Vec<_> = ui.into_messages().collect();
|
||||
assert!(messages
|
||||
.iter()
|
||||
.any(|m| matches!(m, Message::ViewportLeftPress)));
|
||||
assert!(messages
|
||||
.iter()
|
||||
.any(|m| matches!(m, Message::ViewportRightPress)));
|
||||
assert!(messages
|
||||
.iter()
|
||||
.any(|m| matches!(m, Message::ViewportMiddlePress)));
|
||||
}
|
||||
#[test]
|
||||
fn measurement_overlay_stays_inside_canvas_above_command_line() {
|
||||
for anchor in [iced::Point::new(5.0, 5.0), iced::Point::new(790.0, 595.0)] {
|
||||
let mut ui = iced_test::simulator::Simulator::with_size(
|
||||
Default::default(),
|
||||
iced::Size::new(800.0, 600.0),
|
||||
measurement_overlay(anchor, 90.0, "measurement-result".into()),
|
||||
);
|
||||
let bounds = ui
|
||||
.find("measurement-result")
|
||||
.unwrap()
|
||||
.visible_bounds()
|
||||
.unwrap();
|
||||
assert!(bounds.x >= 4.0 && bounds.y >= 4.0);
|
||||
assert!(bounds.x + bounds.width <= 796.0 && bounds.y + bounds.height <= 506.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user