//! Interactive tools (xfig's drawing and editing modes). //! //! A tool receives pointer and key events in Fig coordinates, keeps its //! in-progress state (the rubber band) and commits its result by running a //! command, so every edit is undoable and scriptable. // // Ported in parts from xfig src/w_modepanel.c (mode keys), the drawing and // editing modes src/d_*.c and src/e_*.c (button behaviour, `set_mousefun` // wording) and src/w_canvas.c (snapping) (xfig licence, see LICENSE-xfig). mod chop; mod ctrl; mod cursor; mod mode_messages; mod textedit; pub use cursor::CursorKind; mod compound; pub(crate) mod markers; mod measure; mod tangent; use crate::Region; pub use markers::{mode_mask, object_markers, ObjMask}; use crate::cmd::shape::make_ellipse; use crate::{Engine, EngineError, UiRequest}; mod lengths; use rustfig_geom::{dist_to_segment, Affine, Point, Rect}; use rustfig_model::{Arc, EllipseKind, Justify, Object, ObjectId, ObjectKind, PolyKind, Polyline, Spline, SplineKind}; use serde_json::json; /// The tools, in mode-panel order. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum ToolKind { /// No mode chosen yet: xfig starts like this ("READY. Select a mode or /// load a file"); canvas clicks do nothing. Not a command or a button. None, CircleRadius, CircleDiameter, EllipseRadii, EllipseDiameter, SplineApproxClosed, SplineApprox, SplineInterpClosed, SplineInterp, XSplineClosed, XSpline, Polygon, Polyline, Box, ArcBox, RegularPolygon, Arc, Picture, Text, Glue, Break, OpenCompound, JoinSplit, Chop, /// Trim, split and extend (plan D16). Trim, Scale, Align, Move, Copy, MovePoint, AddPoint, DeletePoint, Delete, Update, Edit, FlipV, FlipH, RotateCcw, RotateCw, Convert, OpenClose, Arrow, /// Add a tangent or normal line to a curve (xfig's `F_TANGENT`). Tangent, /// Measure an angle: three points, or a corner of a line or an arc /// (xfig's `F_ANGLEMEAS`). AngleMeasure, /// Measure the length of lines, arcs and ellipses (`F_LENMEAS`). LengthMeasure, /// Measure the area of polygons, arcs and ellipses (`F_AREAMEAS`). AreaMeasure, /// Plot a function or data (plan E12): drag the plot's frame, then the /// Plot dialog draws it. Plot, } impl ToolKind { /// Modes that pick an object with xfig's `object_search_*` / /// `point_search_*`: Shift+click locates the object instead of acting. pub fn locates(self) -> bool { use ToolKind::*; matches!( self, Break | OpenCompound | JoinSplit | Chop | Trim | Scale | Align | Move | Copy | MovePoint | AddPoint | DeletePoint | Delete | Update | Edit | FlipV | FlipH | RotateCcw | RotateCw | Convert | Arrow | OpenClose | Tangent // xfig's LOC_OBJ on Shift+left in Glue (`e_glue.c`) and the // length/area measures (`e_measure.c`); also Angle measure // (decided 2026-10-22, xfig leaves it blank there). | Glue | AngleMeasure | LengthMeasure | AreaMeasure ) } pub const ALL: [ToolKind; 46] = [ ToolKind::CircleRadius, ToolKind::CircleDiameter, ToolKind::EllipseRadii, ToolKind::EllipseDiameter, ToolKind::SplineApproxClosed, ToolKind::SplineApprox, ToolKind::SplineInterpClosed, ToolKind::SplineInterp, ToolKind::XSplineClosed, ToolKind::XSpline, ToolKind::Polygon, ToolKind::Polyline, ToolKind::Box, ToolKind::ArcBox, ToolKind::RegularPolygon, ToolKind::Arc, ToolKind::Picture, ToolKind::Text, ToolKind::Glue, ToolKind::Break, ToolKind::OpenCompound, ToolKind::JoinSplit, ToolKind::Chop, ToolKind::Trim, ToolKind::Scale, ToolKind::Align, ToolKind::Move, ToolKind::Copy, ToolKind::MovePoint, ToolKind::AddPoint, ToolKind::DeletePoint, ToolKind::Delete, ToolKind::Update, ToolKind::Edit, ToolKind::FlipV, ToolKind::FlipH, ToolKind::RotateCcw, ToolKind::RotateCw, ToolKind::Convert, ToolKind::OpenClose, ToolKind::Arrow, ToolKind::Tangent, ToolKind::AngleMeasure, ToolKind::LengthMeasure, ToolKind::AreaMeasure, ToolKind::Plot, ]; pub fn id(self) -> &'static str { match self { ToolKind::None => "tool.none", ToolKind::CircleRadius => "tool.circle_radius", ToolKind::CircleDiameter => "tool.circle_diameter", ToolKind::EllipseRadii => "tool.ellipse_radii", ToolKind::EllipseDiameter => "tool.ellipse_diameter", ToolKind::SplineApproxClosed => "tool.closed_spline", ToolKind::SplineApprox => "tool.spline", ToolKind::SplineInterpClosed => "tool.closed_interp_spline", ToolKind::SplineInterp => "tool.interp_spline", ToolKind::XSplineClosed => "tool.closed_xspline", ToolKind::XSpline => "tool.xspline", ToolKind::Polygon => "tool.polygon", ToolKind::Polyline => "tool.polyline", ToolKind::Box => "tool.box", ToolKind::ArcBox => "tool.arc_box", ToolKind::RegularPolygon => "tool.regular_polygon", ToolKind::Arc => "tool.arc", ToolKind::Picture => "tool.picture", ToolKind::Text => "tool.text", ToolKind::Glue => "tool.glue", ToolKind::Break => "tool.break", ToolKind::OpenCompound => "tool.open_compound", ToolKind::JoinSplit => "tool.join_split", ToolKind::Chop => "tool.chop", ToolKind::Trim => "tool.trim", ToolKind::Scale => "tool.scale", ToolKind::Align => "tool.align", ToolKind::Move => "tool.move", ToolKind::Copy => "tool.copy", ToolKind::MovePoint => "tool.move_point", ToolKind::AddPoint => "tool.add_point", ToolKind::DeletePoint => "tool.delete_point", ToolKind::Delete => "tool.delete", ToolKind::Update => "tool.update", ToolKind::Edit => "tool.edit", ToolKind::FlipV => "tool.flip_vertical", ToolKind::FlipH => "tool.flip_horizontal", ToolKind::RotateCcw => "tool.rotate_ccw", ToolKind::RotateCw => "tool.rotate_cw", ToolKind::Convert => "tool.convert", ToolKind::OpenClose => "tool.open_close", ToolKind::Arrow => "tool.arrow", ToolKind::Tangent => "tool.tangent", ToolKind::AngleMeasure => "tool.angle_measure", ToolKind::LengthMeasure => "tool.length_measure", ToolKind::AreaMeasure => "tool.area_measure", ToolKind::Plot => "tool.plot", } } pub fn from_id(id: &str) -> Option { ToolKind::ALL.iter().copied().find(|t| t.id() == id) } pub fn label(self) -> &'static str { match self { ToolKind::None => "No mode", ToolKind::CircleRadius => "Circle (radius)", ToolKind::CircleDiameter => "Circle (diameter)", ToolKind::EllipseRadii => "Ellipse (radii)", ToolKind::EllipseDiameter => "Ellipse (diameter)", ToolKind::SplineApproxClosed => "Closed approximated spline", ToolKind::SplineApprox => "Approximated spline", ToolKind::SplineInterpClosed => "Closed interpolated spline", ToolKind::SplineInterp => "Interpolated spline", ToolKind::XSplineClosed => "Closed X-spline", ToolKind::XSpline => "X-spline", ToolKind::Polygon => "Polygon", ToolKind::Polyline => "Polyline", ToolKind::Box => "Box", ToolKind::ArcBox => "Arc-box (rounded)", ToolKind::RegularPolygon => "Regular polygon", ToolKind::Arc => "Arc", ToolKind::Picture => "Picture", ToolKind::Text => "Text", ToolKind::Glue => "Glue into compound", ToolKind::Break => "Break compound", ToolKind::OpenCompound => "Open compound", ToolKind::JoinSplit => "Join/split lines and splines", ToolKind::Chop => "Chop", ToolKind::Trim => "Trim, split and extend", ToolKind::Scale => "Scale", ToolKind::Align => "Align", ToolKind::Move => "Move", ToolKind::Copy => "Copy", ToolKind::MovePoint => "Move point", ToolKind::AddPoint => "Add point", ToolKind::DeletePoint => "Delete point", ToolKind::Delete => "Delete", ToolKind::Update => "Update attributes", ToolKind::Edit => "Edit attributes", ToolKind::FlipV => "Flip up/down", ToolKind::FlipH => "Flip left/right", ToolKind::RotateCcw => "Rotate counter-clockwise", ToolKind::RotateCw => "Rotate clockwise", ToolKind::Convert => "Convert line ↔ spline", ToolKind::OpenClose => "Open/close", ToolKind::Arrow => "Add/remove arrows", ToolKind::Tangent => "Add tangent/normal to curve", ToolKind::AngleMeasure => "Measure angle", ToolKind::LengthMeasure => "Measure length", ToolKind::AreaMeasure => "Measure area", ToolKind::Plot => "Plot", } } /// Whether the right button sets a centre (Rotate) or anchor (Flip). pub fn uses_set_point(self) -> bool { matches!( self, ToolKind::FlipV | ToolKind::FlipH | ToolKind::RotateCcw | ToolKind::RotateCw ) } /// Single-key accelerators: xfig's mode keys (the tooltips in xfig's /// `w_modepanel.c`). xfig's Ctrl-key modes (Scale, Copy, Add point, /// Edit, Rotate clockwise, Picture) are in the UI's xfig keymap, since /// Ctrl+S, Ctrl+C, … are the platform's on Windows and Linux. pub fn shortcut(self) -> &'static str { match self { ToolKind::CircleRadius => "C", ToolKind::EllipseRadii => "E", ToolKind::CircleDiameter => "Shift+C", ToolKind::EllipseDiameter => "Shift+E", ToolKind::SplineApprox => "S", ToolKind::SplineApproxClosed => "Shift+S", ToolKind::SplineInterp => "I", ToolKind::SplineInterpClosed => "Shift+I", ToolKind::Polygon => "P", ToolKind::Polyline => "L", ToolKind::Box => "B", ToolKind::ArcBox => "Shift+B", ToolKind::RegularPolygon => "Shift+P", ToolKind::Arc => "R", ToolKind::Text => "T", ToolKind::Glue => "G", ToolKind::Break => "Shift+G", ToolKind::OpenCompound => "O", ToolKind::JoinSplit => "J", ToolKind::Chop => "x", ToolKind::Trim => "w", ToolKind::Align => "A", ToolKind::Move => "M", ToolKind::MovePoint => "Shift+M", ToolKind::DeletePoint => "Shift+D", ToolKind::Delete => "D", ToolKind::Update => "U", ToolKind::FlipV => "F", ToolKind::FlipH => "Shift+F", ToolKind::RotateCcw => "Shift+R", ToolKind::Convert => "V", ToolKind::Arrow => "Shift+A", ToolKind::Tangent => "n", ToolKind::Plot => "y", _ => "", } } pub fn is_drawing(self) -> bool { matches!( self, ToolKind::CircleRadius | ToolKind::CircleDiameter | ToolKind::EllipseRadii | ToolKind::EllipseDiameter | ToolKind::SplineApproxClosed | ToolKind::SplineApprox | ToolKind::SplineInterpClosed | ToolKind::SplineInterp | ToolKind::XSplineClosed | ToolKind::XSpline | ToolKind::Polygon | ToolKind::Polyline | ToolKind::Box | ToolKind::ArcBox | ToolKind::RegularPolygon | ToolKind::Arc | ToolKind::Picture | ToolKind::Plot | ToolKind::Text ) } /// Tools that collect a list of points. fn spline_kind(self) -> Option<(SplineKind, bool)> { match self { ToolKind::SplineApproxClosed => Some((SplineKind::Approximated, true)), ToolKind::SplineApprox => Some((SplineKind::Approximated, false)), ToolKind::SplineInterpClosed => Some((SplineKind::Interpolated, true)), ToolKind::SplineInterp => Some((SplineKind::Interpolated, false)), ToolKind::XSplineClosed => Some((SplineKind::X, true)), ToolKind::XSpline => Some((SplineKind::X, false)), _ => None, } } fn collects_points(self) -> bool { matches!(self, ToolKind::Polyline | ToolKind::Polygon | ToolKind::Arc) || self.spline_kind().is_some() } fn is_drag_shape(self) -> bool { matches!( self, ToolKind::CircleRadius | ToolKind::CircleDiameter | ToolKind::EllipseRadii | ToolKind::EllipseDiameter | ToolKind::Box | ToolKind::ArcBox | ToolKind::RegularPolygon | ToolKind::Picture | ToolKind::Plot ) } } /// Keys tools react to. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Key { Enter, Escape, Backspace, Delete, Left, Right, /// Start of the text being typed (xfig's `*Text*translations`). Home, /// End of the text being typed. End, /// Select the whole text being typed (E10). SelectAll, /// Control-K: delete from the caret to the end of the text (`d_text.c`). KillToEnd, /// Control-X: delete from the start of the text to the caret. KillToStart, /// Control-W: delete the selected text. KillSelection, /// Control-^: start a superscript or end a subscript. Superscript, /// Control-_: start a subscript or end a superscript. Subscript, } /// xfig's nested super- and subscripts while text is typed (`supersub`, /// `heights`, `work_fontsize`, `orig_x/orig_y` in `d_text.c`). #[derive(Debug, Clone, Default)] pub(crate) struct TextSub { /// Positive: superscript depth; negative: subscript depth. level: i32, /// The line height of each level left, for coming back. heights: [f64; MAX_SUPSUB], /// The font size of the piece being typed after super/subscripting. size: Option, /// Where the text line began (a new line returns there). line_origin: Option, } /// Nested super/subscripts at most (xfig's `MAX_SUPSUB`). const MAX_SUPSUB: usize = 4; /// A sub/superscript is this fraction of the current size (`CSUB_FRAC`). const CSUB_FRAC: f64 = 0.75; /// The shift of a sub/superscript, in line heights (`PSUB_FRAC`). const PSUB_FRAC: f64 = 1.0 - CSUB_FRAC; /// A mouse button as the tools see it. The UI maps two-button mice and /// trackpads onto these (Alt+left is the middle button, Ctrl+left the right). #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum Button { #[default] Left, Middle, Right, } impl Button { pub fn from_name(s: &str) -> Option