From 96ec672d5b57d572e64120558603ca72b6a08649 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Wed, 17 Jun 2026 11:13:12 +0300 Subject: [PATCH] feat(points): point style commands + DDPTYPE dialog, viewport-aware size MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Points could only render as a 1px dot — no way to change PDMODE/PDSIZE. - PDMODE / PDSIZE commands set the header value and rebuild the glyphs. - DDPTYPE opens a Point Style dialog: a 5x4 grid of rendered glyphs (shape x enclosure), a size field, and relative/absolute radios. - Relative (<=0) PDSIZE now sizes from the current world-per-pixel so the marker stays a roughly constant on-screen size across zoom, rebuilding on zoom via the existing camera-generation cache key. Absolute (>0) scales with the world as before. Switching to absolute with a zero size seeds a positive value from the on-screen size so it stays representable. Part of the #100 extensibility epic (point style surfaced in #106). Co-Authored-By: Claude Opus 4.8 --- src/app/commands.rs | 44 +++++++ src/app/mod.rs | 21 ++++ src/app/mtext_editor.rs | 1 + src/app/update.rs | 66 ++++++++++ src/app/view.rs | 10 ++ src/entities/insert.rs | 1 + src/entities/multileader.rs | 1 + src/entities/point.rs | 129 ++++++++++++------- src/modules/draw/draw/point.rs | 4 + src/scene/cache/block_cache.rs | 2 +- src/scene/convert/tessellate.rs | 7 +- src/scene/mod.rs | 3 + src/ui/mod.rs | 1 + src/ui/point_style.rs | 211 ++++++++++++++++++++++++++++++++ 14 files changed, 456 insertions(+), 45 deletions(-) create mode 100644 src/ui/point_style.rs diff --git a/src/app/commands.rs b/src/app/commands.rs index 53086d66d..459b14cf2 100644 --- a/src/app/commands.rs +++ b/src/app/commands.rs @@ -4801,6 +4801,50 @@ impl OpenCADStudio { self.command_line.push_error("Usage: LTSCALE [value]"); } } + cmd if cmd == "PDMODE" || cmd.starts_with("PDMODE ") => { + let val_str = cmd.trim_start_matches("PDMODE").trim(); + if val_str.is_empty() { + let v = self.tabs[i].scene.document.header.point_display_mode; + self.command_line.push_output(&format!("PDMODE = {v}")); + } else if let Ok(v) = val_str.parse::() { + self.push_undo_snapshot(i, "PDMODE"); + self.tabs[i].scene.document.header.point_display_mode = v; + // Point glyphs are built at tessellation time — rebuild them. + self.tabs[i].scene.bump_geometry(); + self.tabs[i].dirty = true; + self.command_line.push_output(&format!("PDMODE set to {v}")); + } else { + self.command_line.push_error( + "Usage: PDMODE [value] (0=dot 1=none 2=+ 3=x 4=tick; +32 circle, +64 square)", + ); + } + } + cmd if cmd == "PDSIZE" || cmd.starts_with("PDSIZE ") => { + let val_str = cmd.trim_start_matches("PDSIZE").trim(); + if val_str.is_empty() { + let v = self.tabs[i].scene.document.header.point_display_size; + self.command_line.push_output(&format!("PDSIZE = {v:.4}")); + } else if let Ok(v) = val_str.parse::() { + self.push_undo_snapshot(i, "PDSIZE"); + self.tabs[i].scene.document.header.point_display_size = v; + self.tabs[i].scene.bump_geometry(); + self.tabs[i].dirty = true; + self.command_line.push_output(&format!("PDSIZE set to {v:.4}")); + } else { + self.command_line.push_error( + "Usage: PDSIZE [value] (>0 absolute size, <0 percent of viewport, 0 default)", + ); + } + } + cmd if cmd == "DDPTYPE" => { + // The dialog shows the magnitude; the sign (relative/absolute) + // is driven by the radio buttons. A positive PDSIZE is absolute; + // zero or negative is relative. + let pdsize = self.tabs[i].scene.document.header.point_display_size; + self.point_size_relative = pdsize <= 0.0; + self.point_size_buf = format!("{}", pdsize.abs()); + self.active_modal = Some(super::ModalKind::PointStyle); + } cmd if cmd == "LWDISPLAY" || cmd.starts_with("LWDISPLAY ") => { let val_str = cmd.trim_start_matches("LWDISPLAY").trim(); let parsed: Result, ()> = diff --git a/src/app/mod.rs b/src/app/mod.rs index 92aaa10b0..14ee0ebdc 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -297,6 +297,12 @@ pub(super) struct OpenCADStudio { /// keep their manifest listed but drop their ribbon tab and command /// dispatch. Persisted via [`settings::UserSettings::disabled_plugins`]. disabled_plugins: rustc_hash::FxHashSet, + /// PDSIZE text buffer for the Point Style (DDPTYPE) dialog. + point_size_buf: String, + /// Point Style size mode: `true` = relative to screen, `false` = absolute. + /// Tracked separately from the PDSIZE sign so a size of 0 (sign-less) still + /// remembers which radio is active. + point_size_relative: bool, /// New-release notification window — opened on startup when the /// GitHub releases API reports a newer version than this build. /// First-launch "make Open CAD Studio the default for .dwg/.dxf?" prompt @@ -603,6 +609,7 @@ pub enum ModalKind { Unsaved, SaveDialog, AssocPrompt, + PointStyle, } /// Identifies a DimStyle field that can be edited in the dialog. @@ -1031,6 +1038,18 @@ pub enum Message { PluginManagerClose, /// Enable (`true`) or disable (`false`) the plugin with this id. SetPluginEnabled(String, bool), + // ── Point Style (DDPTYPE) dialog ────────────────────────────────────── + /// Set the full PDMODE value from a glyph-grid cell. + PointStyleSetMode(i16), + /// Choose size mode: `true` = relative to screen (PDSIZE < 0), `false` = + /// absolute units (PDSIZE > 0). + PointStyleSizeRelative(bool), + /// Edit the point-size text field (magnitude only). + PointStyleSizeInput(String), + /// Commit the point-size text field to the header with the current sign. + PointStyleApplySize, + /// Apply the point-size field and close the dialog (OK button). + PointStyleOk, // ── Quick Select / Select Similar ─────────────────────────────────── /// Extend the current selection with every entity in the active /// layout that matches a selected entity by (type, layer). @@ -1442,6 +1461,8 @@ impl OpenCADStudio { modal_drag_last: None, modal_dragging: false, disabled_plugins: rustc_hash::FxHashSet::default(), + point_size_buf: String::new(), + point_size_relative: true, default_assoc_prompted: false, update_notice_version: None, update_notice_body: None, diff --git a/src/app/mtext_editor.rs b/src/app/mtext_editor.rs index 3efc39c93..e2422391e 100644 --- a/src/app/mtext_editor.rs +++ b/src/app/mtext_editor.rs @@ -408,6 +408,7 @@ impl super::OpenCADStudio { 1.0, woff, anno, + None, ); // Per-character boxes in the same offset-subtracted frame as the // preview wires (tessellate removes world_offset; mirror that). diff --git a/src/app/update.rs b/src/app/update.rs index b1b232ab0..9312061cd 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -178,6 +178,50 @@ impl OpenCADStudio { self.disabled_plugins.clone() } + /// Write PDSIZE from the dialog buffer with the current relative/absolute + /// sign. A relative size is stored negative; absolute positive. Switching to + /// absolute with an empty/zero size seeds a positive value from the current + /// on-screen size so the point stays representable (PDSIZE 0 always reads as + /// relative, so absolute needs a non-zero magnitude). + fn apply_point_size(&mut self) { + let i = self.active_tab; + let mut mag = self + .point_size_buf + .trim() + .parse::() + .unwrap_or(0.0) + .abs(); + if !self.point_size_relative && mag == 0.0 { + let wpp = self.tabs[i].scene.world_per_pixel().unwrap_or(0.0); + mag = if wpp > 0.0 { + crate::entities::point::relative_world_size(0.0, wpp) + } else { + 1.0 + }; + self.point_size_buf = format!("{mag:.4}"); + } + let next = if self.point_size_relative { -mag } else { mag }; + self.push_undo_snapshot(i, "PDSIZE"); + self.tabs[i].scene.document.header.point_display_size = next; + self.tabs[i].scene.bump_geometry(); + self.tabs[i].dirty = true; + } + + /// Replace the `mask` bits of PDMODE with `value`, rebuild the point glyphs + /// and mark the document dirty. Used by the Point Style (DDPTYPE) dialog. + fn set_point_mode_bits(&mut self, mask: i16, value: i16) { + let i = self.active_tab; + let cur = self.tabs[i].scene.document.header.point_display_mode; + let next = (cur & !mask) | (value & mask); + if next == cur { + return; + } + self.push_undo_snapshot(i, "PDMODE"); + self.tabs[i].scene.document.header.point_display_mode = next; + self.tabs[i].scene.bump_geometry(); + self.tabs[i].dirty = true; + } + /// Write preferences to disk only when they differ from the last write, /// so a toggle persists immediately without thrashing the file. fn persist_settings_if_changed(&mut self) { @@ -5499,6 +5543,28 @@ impl OpenCADStudio { self.persist_settings_if_changed(); Task::none() } + Message::PointStyleSetMode(mode) => { + self.set_point_mode_bits(!0, mode); + Task::none() + } + Message::PointStyleSizeRelative(relative) => { + self.point_size_relative = relative; + self.apply_point_size(); + Task::none() + } + Message::PointStyleSizeInput(s) => { + self.point_size_buf = s; + Task::none() + } + Message::PointStyleApplySize => { + self.apply_point_size(); + Task::none() + } + Message::PointStyleOk => { + self.apply_point_size(); + self.close_active_modal(); + Task::none() + } Message::EnterViewport(handle) => { let i = self.active_tab; diff --git a/src/app/view.rs b/src/app/view.rs index 202984c23..b1b6b7867 100644 --- a/src/app/view.rs +++ b/src/app/view.rs @@ -1100,6 +1100,7 @@ impl OpenCADStudio { AssocPrompt => (440, 210), Unsaved => (420, 160), SaveDialog => (560, 480), + PointStyle => (360, 470), }; Some((w as f32 + EXTRA_W, h as f32 + EXTRA_H)) } @@ -1572,6 +1573,15 @@ impl OpenCADStudio { }; sized(unsaved_changes_dialog_window(&tab_name), 420, 160) } + super::ModalKind::PointStyle => sized( + crate::ui::point_style::view_window( + self.tabs[self.active_tab].scene.document.header.point_display_mode, + self.point_size_relative, + &self.point_size_buf, + ), + 360, + 470, + ), super::ModalKind::SaveDialog => sized( save_as_dialog_window( &self.save_dialog_filename, diff --git a/src/entities/insert.rs b/src/entities/insert.rs index 1d6989cc3..7cb445f92 100644 --- a/src/entities/insert.rs +++ b/src/entities/insert.rs @@ -181,6 +181,7 @@ pub(crate) fn append_insert_attribute_wires( sub_lw_px, world_offset, anno_scale, + None, ); // Use the INSERT's handle so selection / picking groups attribute // text with the parent insert instead of treating it as a stray text. diff --git a/src/entities/multileader.rs b/src/entities/multileader.rs index 8ac725754..04a839397 100644 --- a/src/entities/multileader.rs +++ b/src/entities/multileader.rs @@ -1340,6 +1340,7 @@ impl MultiLeaderTess for MultiLeader { leader_lw_px, world_offset, 1.0, + None, ); for w in &mut sub_wires { w.name = name.clone(); diff --git a/src/entities/point.rs b/src/entities/point.rs index 159fd4276..8aa9d2a09 100644 --- a/src/entities/point.rs +++ b/src/entities/point.rs @@ -1,4 +1,5 @@ use acadrust::entities::Point; +use acadrust::EntityType; use glam::Vec3; use truck_modeling::{builder, Point3}; @@ -9,20 +10,96 @@ use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject}; use crate::scene::model::object::{GripApply, GripDef, PropSection}; use crate::scene::model::wire_model::SnapHint; -/// Resolve PDSIZE (negative = % of viewport, 0 = 5% default, positive = world). -/// We don't know the viewport height at tessellation time, so percentages and -/// the 0 default expand to a small fixed world size as a best-effort fallback. +/// Nominal viewport height (px) used to turn a relative PDSIZE percentage into +/// an on-screen pixel size. The exact viewport height isn't threaded into +/// tessellation; this reference keeps relative points a roughly constant +/// fraction of the screen across zoom levels. +const REL_REF_PX: f64 = 600.0; + +/// Resolve a positive (absolute) PDSIZE to a world size. Relative/zero PDSIZE +/// is handled by [`relative_truck`] when a world-per-pixel factor is available; +/// without one it falls back to a small fixed world size. fn pdsize_world(pdsize: f64) -> f64 { if pdsize > 0.0 { pdsize } else { - // Both PDSIZE = 0 and negative (relative) fall back to a sensible - // visible default until viewport-aware sizing is wired up. 2.0 } } -fn point_glyph(cx: f64, cy: f64, z: f64, pdmode: i16, pdsize: f64) -> Vec<[f64; 3]> { +/// Build the truck entity for a point given the glyph half-size `s` in world +/// units. Shared by the header-driven path ([`to_truck`]) and the +/// viewport-aware relative path ([`relative_truck`]). +fn point_truck(pt: &Point, pdmode: i16, s: f64) -> TruckEntity { + let normal = (pt.normal.x, pt.normal.y, pt.normal.z); + let (wx, wy, wz) = crate::scene::view::transform::ocs_point_to_wcs( + (pt.location.x, pt.location.y, pt.location.z), + normal, + ); + let snap = Vec3::new(wx as f32, wy as f32, wz as f32); + if pdmode == 0 { + // Default: a single vertex (driver handles the dot pixel). + let p = Point3::new(wx, wy, wz); + return TruckEntity { + object: TruckObject::Point(builder::vertex(p)), + snap_pts: vec![(snap, SnapHint::Node)], + tangent_geoms: vec![], + key_vertices: vec![], + fill_tris: vec![], + }; + } + let pts = point_glyph(wx, wy, wz, pdmode, s); + if pts.is_empty() { + // PDMODE 1 = nothing — emit an empty Lines wire so picking still works. + return TruckEntity { + object: TruckObject::Lines(vec![]), + snap_pts: vec![(snap, SnapHint::Node)], + tangent_geoms: vec![], + key_vertices: vec![[wx, wy, wz]], + fill_tris: vec![], + }; + } + TruckEntity { + object: TruckObject::Lines(pts), + snap_pts: vec![(snap, SnapHint::Node)], + tangent_geoms: vec![], + key_vertices: vec![[wx, wy, wz]], + fill_tris: vec![], + } +} + +/// Viewport-aware override for a relative (≤ 0) PDSIZE: size the glyph from the +/// world-per-pixel factor so the point stays a roughly constant on-screen size +/// across zoom. Returns `None` for an absolute PDSIZE, the size-independent +/// default dot (PDMODE 0), or when no `wpp` is available — the caller then uses +/// the normal header-driven path. +pub fn relative_truck( + entity: &EntityType, + document: &acadrust::CadDocument, + wpp: Option, +) -> Option { + let EntityType::Point(pt) = entity else { + return None; + }; + let pdsize = document.header.point_display_size; + let pdmode = document.header.point_display_mode; + if pdsize > 0.0 || pdmode == 0 { + return None; + } + let wpp = wpp.filter(|w| *w > 0.0)?; + Some(point_truck(pt, pdmode, relative_world_size(pdsize, wpp) * 0.5)) +} + +/// Full on-screen glyph size (world units) for a relative (≤ 0) PDSIZE at the +/// given world-per-pixel factor. PDSIZE 0 is the 5% default; negative is the +/// percentage. Used both for rendering and to seed an absolute size when the +/// user switches the Point Style dialog to absolute units. +pub fn relative_world_size(pdsize: f64, wpp: f32) -> f64 { + let pct = if pdsize == 0.0 { 5.0 } else { -pdsize }; + (pct / 100.0) * REL_REF_PX * wpp as f64 +} + +fn point_glyph(cx: f64, cy: f64, z: f64, pdmode: i16, s_half: f64) -> Vec<[f64; 3]> { // PDMODE bits: // shape: 0=dot, 1=nothing, 2='+', 3='×', 4='|' // +32 = enclose in a circle @@ -31,7 +108,7 @@ fn point_glyph(cx: f64, cy: f64, z: f64, pdmode: i16, pdsize: f64) -> Vec<[f64; let shape = (pdmode & 0x0F) as i32; let circle = (pdmode & 32) != 0; let square = (pdmode & 64) != 0; - let s = pdsize_world(pdsize) * 0.5; + let s = s_half; let nan = [f64::NAN, f64::NAN, f64::NAN]; let mut pts: Vec<[f64; 3]> = Vec::new(); let mut push_seg = |a: [f64; 3], b: [f64; 3]| { @@ -92,43 +169,9 @@ fn point_glyph(cx: f64, cy: f64, z: f64, pdmode: i16, pdsize: f64) -> Vec<[f64; } fn to_truck(pt: &Point, document: &acadrust::CadDocument) -> TruckEntity { - let normal = (pt.normal.x, pt.normal.y, pt.normal.z); - let (wx, wy, wz) = crate::scene::view::transform::ocs_point_to_wcs( - (pt.location.x, pt.location.y, pt.location.z), - normal, - ); - let snap = Vec3::new(wx as f32, wy as f32, wz as f32); let pdmode = document.header.point_display_mode; - let pdsize = document.header.point_display_size; - if pdmode == 0 { - // Default: a single vertex (driver handles the dot pixel). - let p = Point3::new(wx, wy, wz); - return TruckEntity { - object: TruckObject::Point(builder::vertex(p)), - snap_pts: vec![(snap, SnapHint::Node)], - tangent_geoms: vec![], - key_vertices: vec![], - fill_tris: vec![], - }; - } - let pts = point_glyph(wx, wy, wz, pdmode, pdsize); - if pts.is_empty() { - // PDMODE 1 = nothing — emit an empty Lines wire so picking still works. - return TruckEntity { - object: TruckObject::Lines(vec![]), - snap_pts: vec![(snap, SnapHint::Node)], - tangent_geoms: vec![], - key_vertices: vec![[wx, wy, wz]], - fill_tris: vec![], - }; - } - TruckEntity { - object: TruckObject::Lines(pts), - snap_pts: vec![(snap, SnapHint::Node)], - tangent_geoms: vec![], - key_vertices: vec![[wx, wy, wz]], - fill_tris: vec![], - } + let s = pdsize_world(document.header.point_display_size) * 0.5; + point_truck(pt, pdmode, s) } fn grips(pt: &Point) -> Vec { diff --git a/src/modules/draw/draw/point.rs b/src/modules/draw/draw/point.rs index 14fad795e..2c4e292f0 100644 --- a/src/modules/draw/draw/point.rs +++ b/src/modules/draw/draw/point.rs @@ -59,3 +59,7 @@ impl CadCommand for PointCommand { // ── Autocomplete registry ───────────────────────────────── inventory::submit!(crate::command::CommandRegistration { names: &["PO", "POINT"] }); // PointCommand +// Point display style system variables + dialog. +inventory::submit!(crate::command::CommandRegistration { + names: &["PDMODE", "PDSIZE", "DDPTYPE"] +}); diff --git a/src/scene/cache/block_cache.rs b/src/scene/cache/block_cache.rs index e33e9bcc5..3746015a9 100644 --- a/src/scene/cache/block_cache.rs +++ b/src/scene/cache/block_cache.rs @@ -450,7 +450,7 @@ fn tessellate_sub_local( // top-level MTEXT outside blocks the colour split is preserved via the // hot path in `tessellate_entity`. let mut wires_out = tessellate::tessellate( - doc, h, sub, false, sub_color, pat_len, pat, lw_px, local_offset, anno_scale, + doc, h, sub, false, sub_color, pat_len, pat, lw_px, local_offset, anno_scale, None, ); if wires_out.is_empty() { return None; diff --git a/src/scene/convert/tessellate.rs b/src/scene/convert/tessellate.rs index 2005597b8..8ee326d52 100644 --- a/src/scene/convert/tessellate.rs +++ b/src/scene/convert/tessellate.rs @@ -42,6 +42,7 @@ pub fn tessellate( line_weight_px: f32, world_offset: [f64; 3], anno_scale: f32, + world_per_pixel: Option, ) -> Vec { let color = if selected { WireModel::SELECTED @@ -87,7 +88,11 @@ pub fn tessellate( } // ── Try the truck path first ─────────────────────────────────────────── - if let Some(te) = convert(entity, document) { + // Relative-PDSIZE points size their glyph from the current zoom so they + // stay a roughly constant on-screen size; otherwise the header-driven path. + let te = crate::entities::point::relative_truck(entity, document, world_per_pixel) + .or_else(|| convert(entity, document)); + if let Some(te) = te { match te.object { // ── Text / MText: pre-tessellated glyph strokes ─────────────── // diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 61a3db3f3..1b3e39c7f 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -7109,6 +7109,7 @@ fn tessellate_entity( 1.5, world_offset, 1.0, + world_per_pixel, ); let ab = entity_aabb(e, world_offset); for w in &mut wires { @@ -7423,6 +7424,7 @@ fn tessellate_entity( sub_line_weight_px, world_offset, anno_scale, + world_per_pixel, ); for w in &mut wires { w.name = h.value().to_string(); @@ -7584,6 +7586,7 @@ fn tessellate_entity( line_weight_px, world_offset, anno_scale, + world_per_pixel, ); for b in &mut bases { b.aci = aci; diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 90f8ec234..d192bcad7 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -19,6 +19,7 @@ pub mod overlay; pub mod page_setup; pub mod plotstyle; pub mod plugin_manager; +pub mod point_style; pub mod properties; pub mod ribbon; pub mod scale_popup; diff --git a/src/ui/point_style.rs b/src/ui/point_style.rs new file mode 100644 index 000000000..d2a5ee6ad --- /dev/null +++ b/src/ui/point_style.rs @@ -0,0 +1,211 @@ +//! Point Style (DDPTYPE) dialog — pick a PDMODE glyph from a grid and set the +//! point size (PDSIZE), relative to the screen or in absolute units. +//! +//! Changes apply live to the active document header; the renderer rebuilds the +//! point glyphs (see `entities::point`). Command entry (`PDMODE` / `PDSIZE`) +//! does the same without the dialog. + +use crate::app::Message; +use iced::widget::{button, canvas, column, container, radio, row, text, text_input, Space}; +use iced::{mouse, Background, Border, Color, Element, Length, Point, Rectangle, Size, Theme}; + +const BG: Color = Color { r: 0.15, g: 0.15, b: 0.15, a: 1.0 }; +const WHITE: Color = Color { r: 0.92, g: 0.92, b: 0.92, a: 1.0 }; +const DIM: Color = Color { r: 0.55, g: 0.55, b: 0.55, a: 1.0 }; +const ACCENT: Color = Color { r: 0.30, g: 0.62, b: 0.95, a: 1.0 }; +const CELL: Color = Color { r: 0.20, g: 0.20, b: 0.20, a: 1.0 }; +const CELL_SEL: Color = Color { r: 0.30, g: 0.62, b: 0.95, a: 1.0 }; +const GLYPH: Color = Color { r: 0.90, g: 0.90, b: 0.90, a: 1.0 }; + +const CELL_PX: f32 = 44.0; + +/// Glyph-grid columns (low-nibble shape) × rows (enclosure bits): +/// shapes: 0=dot, 1=none, 2='+', 3='×', 4='|' +/// enclosures: 0=none, 32=circle, 64=square, 96=both +const ENCLOSURES: [i16; 4] = [0, 32, 64, 96]; +const SHAPES: [i16; 5] = [0, 1, 2, 3, 4]; + +/// Canvas that renders a single PDMODE glyph inside a grid cell. +struct GlyphCanvas { + mode: i16, +} + +impl canvas::Program for GlyphCanvas { + type State = (); + + fn draw( + &self, + _state: &(), + renderer: &iced::Renderer, + _theme: &Theme, + bounds: Rectangle, + _cursor: mouse::Cursor, + ) -> Vec { + let mut frame = canvas::Frame::new(renderer, bounds.size()); + let (cx, cy) = (bounds.width * 0.5, bounds.height * 0.5); + let r = bounds.width.min(bounds.height) * 0.30; + let stroke = canvas::Stroke { + width: 1.4, + style: canvas::Style::Solid(GLYPH), + ..Default::default() + }; + let line = |a: Point, b: Point| canvas::Path::line(a, b); + + match self.mode & 0x0F { + 0 => frame.fill(&canvas::Path::circle(Point::new(cx, cy), 2.4), GLYPH), + 1 => {} + 2 => { + frame.stroke(&line(Point::new(cx - r, cy), Point::new(cx + r, cy)), stroke.clone()); + frame.stroke(&line(Point::new(cx, cy - r), Point::new(cx, cy + r)), stroke.clone()); + } + 3 => { + frame.stroke( + &line(Point::new(cx - r, cy - r), Point::new(cx + r, cy + r)), + stroke.clone(), + ); + frame.stroke( + &line(Point::new(cx - r, cy + r), Point::new(cx + r, cy - r)), + stroke.clone(), + ); + } + 4 => frame.stroke(&line(Point::new(cx, cy - r), Point::new(cx, cy + r)), stroke.clone()), + _ => {} + } + if self.mode & 32 != 0 { + frame.stroke(&canvas::Path::circle(Point::new(cx, cy), r), stroke.clone()); + } + if self.mode & 64 != 0 { + let sq = canvas::Path::rectangle(Point::new(cx - r, cy - r), Size::new(2.0 * r, 2.0 * r)); + frame.stroke(&sq, stroke.clone()); + } + vec![frame.into_geometry()] + } +} + +fn cell<'a>(value: i16, selected: bool) -> Element<'a, Message> { + let glyph = canvas(GlyphCanvas { mode: value }) + .width(Length::Fixed(CELL_PX)) + .height(Length::Fixed(CELL_PX)); + button(glyph) + .padding(0) + .on_press(Message::PointStyleSetMode(value)) + .style(move |_: &Theme, status| { + let bg = if selected { + CELL_SEL + } else if matches!(status, button::Status::Hovered | button::Status::Pressed) { + Color { r: 0.28, g: 0.28, b: 0.28, a: 1.0 } + } else { + CELL + }; + button::Style { + background: Some(Background::Color(bg)), + border: Border { + color: Color { r: 0.35, g: 0.35, b: 0.35, a: 1.0 }, + width: 1.0, + radius: 3.0.into(), + }, + ..Default::default() + } + }) + .into() +} + +fn field_style(_: &Theme, _: text_input::Status) -> text_input::Style { + text_input::Style { + background: Background::Color(Color { r: 0.1, g: 0.1, b: 0.1, a: 1.0 }), + border: Border { + color: Color { r: 0.3, g: 0.3, b: 0.3, a: 1.0 }, + width: 1.0, + radius: 4.0.into(), + }, + icon: DIM, + placeholder: DIM, + value: WHITE, + selection: ACCENT, + } +} + +pub fn view_window<'a>(pdmode: i16, relative: bool, size_buf: &str) -> Element<'a, Message> { + // Glyph grid: a row per enclosure, a cell per shape. + let mut grid = column![].spacing(6); + for enc in ENCLOSURES { + let mut r = row![].spacing(6); + for sh in SHAPES { + let value = enc + sh; + r = r.push(cell(value, value == pdmode)); + } + grid = grid.push(r); + } + + let size_row = row![ + text("Point Size:").size(13).color(WHITE), + Space::new().width(10), + text_input("0", size_buf) + .on_input(Message::PointStyleSizeInput) + .on_submit(Message::PointStyleApplySize) + .style(field_style) + .size(13) + .width(110), + Space::new().width(6), + text(if relative { "%" } else { "units" }).size(12).color(DIM), + ] + .align_y(iced::Center); + + let radios = column![ + radio( + "Set Size Relative to Screen", + true, + Some(relative), + Message::PointStyleSizeRelative, + ) + .size(15) + .text_size(13), + radio( + "Set Size in Absolute Units", + false, + Some(relative), + Message::PointStyleSizeRelative, + ) + .size(15) + .text_size(13), + ] + .spacing(6); + + let ok = button(text("OK").size(13).color(WHITE)) + .padding([5, 22]) + .on_press(Message::PointStyleOk) + .style(|_: &Theme, status| { + let bg = if matches!(status, button::Status::Hovered | button::Status::Pressed) { + Color { r: 0.32, g: 0.55, b: 0.85, a: 1.0 } + } else { + ACCENT + }; + button::Style { + background: Some(Background::Color(bg)), + text_color: WHITE, + border: Border { radius: 4.0.into(), ..Default::default() }, + ..Default::default() + } + }); + + container( + column![ + text("Point Style").size(18).color(WHITE), + Space::new().height(6), + grid, + Space::new().height(12), + size_row, + Space::new().height(8), + radios, + Space::new().height(12), + row![Space::new().width(Length::Fill), ok], + ] + .spacing(4) + .padding(20), + ) + .style(|_: &Theme| container::Style { + background: Some(Background::Color(BG)), + ..Default::default() + }) + .into() +}