2026-03-22 22:44:32 +03:00
|
|
|
use super::document::DocumentTab;
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
use super::document::{DynComponent, DynFieldEntry};
|
2026-03-22 22:44:32 +03:00
|
|
|
use super::helpers::grid_plane_from_camera;
|
2026-05-12 10:36:15 +03:00
|
|
|
use super::history::history_dropdown_labels;
|
2026-05-23 17:16:15 +03:00
|
|
|
use super::{Message, OpenCADStudio};
|
2026-06-16 11:33:46 +03:00
|
|
|
use crate::scene::pick::grip::{grips_to_screen, grips_to_screen_paper};
|
|
|
|
|
use crate::scene::view::viewport_pane::ViewportPane;
|
2026-05-12 10:36:15 +03:00
|
|
|
use crate::scene::{VIEWCUBE_DRAW_PX, VIEWCUBE_PAD};
|
2026-03-22 22:44:32 +03:00
|
|
|
use crate::ui::overlay;
|
2026-05-12 10:36:15 +03:00
|
|
|
use iced::widget::{
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
button, column, container, mouse_area, pick_list, row, shader, stack, text, text_input, Row,
|
|
|
|
|
Space,
|
2026-05-12 10:36:15 +03:00
|
|
|
};
|
2026-03-22 22:44:32 +03:00
|
|
|
use iced::window;
|
|
|
|
|
use iced::{keyboard, Background, Border, Color, Element, Fill, Subscription, Task, Theme};
|
|
|
|
|
|
|
|
|
|
const VIEWCUBE_HIT_SIZE: f32 = VIEWCUBE_DRAW_PX;
|
|
|
|
|
|
2026-05-27 00:08:37 +03:00
|
|
|
/// `pick_list` requires its items to implement `Display`; acadrust's
|
|
|
|
|
/// `ViewportRenderMode` enum carries the raw DXF integers, not a label,
|
|
|
|
|
/// so wrap it locally with a friendly name renderer.
|
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
|
|
|
pub(super) struct RenderModeChoice(pub acadrust::entities::ViewportRenderMode);
|
|
|
|
|
|
|
|
|
|
impl std::fmt::Display for RenderModeChoice {
|
|
|
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
|
|
|
use acadrust::entities::ViewportRenderMode as M;
|
|
|
|
|
f.write_str(match self.0 {
|
|
|
|
|
M::Wireframe2D => "Wireframe 2D",
|
|
|
|
|
M::Wireframe3D => "Wireframe 3D",
|
|
|
|
|
M::HiddenLine => "Hidden Line",
|
|
|
|
|
M::FlatShaded => "Flat Shaded",
|
|
|
|
|
M::GouraudShaded => "Gouraud Shaded",
|
|
|
|
|
M::FlatShadedWithEdges => "Flat Shaded + Edges",
|
|
|
|
|
M::GouraudShadedWithEdges => "Gouraud Shaded + Edges",
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 17:16:15 +03:00
|
|
|
impl OpenCADStudio {
|
2026-06-22 02:03:34 +03:00
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
2026-03-22 22:44:32 +03:00
|
|
|
pub fn view(&self, window_id: window::Id) -> Element<'_, Message> {
|
2026-04-08 22:36:28 +03:00
|
|
|
// ── Floating panel windows ─────────────────────────────────────────
|
2026-06-16 23:59:38 +03:00
|
|
|
// All dialogs are in-canvas modals now (Plan B); view_main stacks the
|
|
|
|
|
// active one. `window_id` is unused — there is only the main window.
|
|
|
|
|
let _ = window_id;
|
2026-06-16 13:16:35 +03:00
|
|
|
self.view_main()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The primary window: viewport, ribbon, tab bar, status bar. Split out of
|
|
|
|
|
/// `view` so the single-window web build can render it directly, bypassing
|
|
|
|
|
/// the multi-window id dispatch above (the web build has no extra windows).
|
|
|
|
|
pub fn view_main(&self) -> Element<'_, Message> {
|
2026-03-22 22:44:32 +03:00
|
|
|
let i = self.active_tab;
|
|
|
|
|
let tab = &self.tabs[i];
|
|
|
|
|
let is_paper = tab.scene.current_layout != "Model";
|
2026-06-02 20:33:51 +03:00
|
|
|
// Start tab: render welcome page in place of the viewport.
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
// Surrounding chrome (tab bar, status bar) stays; the welcome widget
|
|
|
|
|
// returned here also flags the rest of `view` to skip drawing-only
|
|
|
|
|
// overlays via `tab.is_start`.
|
2026-06-02 20:33:51 +03:00
|
|
|
// Unified GPU widget for both layouts. A paper layout renders through
|
|
|
|
|
// the same shader as model space: a full-canvas top-locked "sheet"
|
|
|
|
|
// viewport draws the layout's own geometry (white sheet + entities +
|
|
|
|
|
// borders) and the floating content viewports blit on top.
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
let viewport_3d: Element<'_, Message> = if tab.is_start {
|
|
|
|
|
start_page_view()
|
2026-04-23 02:18:41 +03:00
|
|
|
} else {
|
2026-05-27 00:08:37 +03:00
|
|
|
shader(ViewportPane::model(
|
|
|
|
|
&tab.scene,
|
|
|
|
|
self.show_viewcube,
|
|
|
|
|
tab.render_mode,
|
|
|
|
|
))
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
.width(Fill)
|
|
|
|
|
.height(Fill)
|
|
|
|
|
.into()
|
2026-04-23 02:18:41 +03:00
|
|
|
};
|
2026-03-22 22:44:32 +03:00
|
|
|
|
|
|
|
|
let selection_overlay = {
|
|
|
|
|
let sel = tab.scene.selection.borrow().clone();
|
|
|
|
|
let snap_info = tab.snap_result.map(|s| (s.screen, s.snap_type));
|
|
|
|
|
|
|
|
|
|
let grips: Vec<overlay::GripMarker> =
|
|
|
|
|
if tab.active_cmd.is_none() && !tab.selected_grips.is_empty() {
|
|
|
|
|
let (vw, vh) = tab.scene.selection.borrow().vp_size;
|
2026-05-28 09:45:19 +03:00
|
|
|
// Overlays project through the active tile's camera, so
|
|
|
|
|
// they must use the active tile's screen rectangle (with
|
|
|
|
|
// its canvas offset) — not the whole canvas — or they
|
|
|
|
|
// land in the wrong place in a tiled layout.
|
|
|
|
|
let bounds = tab.scene.active_model_tile_bounds(vw, vh);
|
2026-03-22 22:44:32 +03:00
|
|
|
let sel_h = tab.selected_handle;
|
2026-05-01 12:16:46 +03:00
|
|
|
let screen_grips = if is_paper {
|
|
|
|
|
let cam = tab.scene.camera.borrow();
|
|
|
|
|
let aspect = if vh > 0.0 { vw / vh } else { 1.0 };
|
|
|
|
|
let half_h = cam.ortho_size();
|
|
|
|
|
let half_w = half_h * aspect;
|
2026-06-22 20:28:30 +03:00
|
|
|
let tx = cam.target.x as f32;
|
|
|
|
|
let ty = cam.target.y as f32;
|
2026-05-01 12:16:46 +03:00
|
|
|
drop(cam);
|
|
|
|
|
grips_to_screen_paper(&tab.selected_grips, tx, ty, half_w, half_h, bounds)
|
|
|
|
|
} else {
|
2026-06-23 22:57:31 +03:00
|
|
|
let cam = tab.scene.camera.borrow();
|
|
|
|
|
grips_to_screen(&tab.selected_grips, &cam, bounds)
|
2026-05-01 12:16:46 +03:00
|
|
|
};
|
|
|
|
|
screen_grips
|
2026-03-22 22:44:32 +03:00
|
|
|
.into_iter()
|
refactor(grip): square-only marker set + oriented stretch handles
Phase 1 of bringing the grip vocabulary in line with the standard
CAD convention: drop the per-position-type shape encoding (Diamond
for translate, Circle for parameter) and use a plain square for
every endpoint / vertex / centre grip. The behavioural midpoint vs
vertex distinction stays — it lives on `GripDef::is_midpoint` and
drives `Translate` vs `Absolute` in the grip-edit dispatcher — only
the marker shape collapses.
- `GripShape` keeps `Square`, `Rectangle`, `Triangle`. Diamond /
Circle are gone.
- `GripDef` gains `dir: Option<[f32; 2]>` — a world-XY direction
vector consumed by `Rectangle` to orient the box along its
segment.
- `entities::common` exposes `square_grip`, `center_grip` (same
square marker, flagged as translate), `rectangle_grip(id, world,
dir)` for oriented mid-segment handles, and a kept-for-Phase-2
`triangle_grip`.
- `overlay::GripMarker` / `grips_to_screen[_paper]` thread `dir`
through to the canvas painter; the Rectangle path rotates the
box around the grip centre using the world-XY direction (flipping
the sin for screen-Y).
Every existing `diamond_grip(id, w)` call becomes `center_grip(id,
w)` — the visual change is square-replacing-diamond at line
midpoints / arc midpoints / circle centres / ellipse centres /
viewport / hatch loop centroids / dimension grips / image corners /
multileader / ray / underlay / ole2frame / solid3d.
`LwPolyline` grows a stretch handle on every segment (straight or
arc), drawn as the new oriented rectangle. The grip-edit path
translates both segment endpoints for straight segments and
adjusts the arc bulge from the new midpoint for arc segments.
2026-05-29 02:07:34 +03:00
|
|
|
.filter(|(_, screen, _, _, _)| {
|
2026-03-22 22:44:32 +03:00
|
|
|
screen.x.is_finite()
|
|
|
|
|
&& screen.y.is_finite()
|
|
|
|
|
&& screen.x >= -bounds.width
|
|
|
|
|
&& screen.x <= bounds.width * 2.0
|
|
|
|
|
&& screen.y >= -bounds.height
|
|
|
|
|
&& screen.y <= bounds.height * 2.0
|
|
|
|
|
})
|
refactor(grip): square-only marker set + oriented stretch handles
Phase 1 of bringing the grip vocabulary in line with the standard
CAD convention: drop the per-position-type shape encoding (Diamond
for translate, Circle for parameter) and use a plain square for
every endpoint / vertex / centre grip. The behavioural midpoint vs
vertex distinction stays — it lives on `GripDef::is_midpoint` and
drives `Translate` vs `Absolute` in the grip-edit dispatcher — only
the marker shape collapses.
- `GripShape` keeps `Square`, `Rectangle`, `Triangle`. Diamond /
Circle are gone.
- `GripDef` gains `dir: Option<[f32; 2]>` — a world-XY direction
vector consumed by `Rectangle` to orient the box along its
segment.
- `entities::common` exposes `square_grip`, `center_grip` (same
square marker, flagged as translate), `rectangle_grip(id, world,
dir)` for oriented mid-segment handles, and a kept-for-Phase-2
`triangle_grip`.
- `overlay::GripMarker` / `grips_to_screen[_paper]` thread `dir`
through to the canvas painter; the Rectangle path rotates the
box around the grip centre using the world-XY direction (flipping
the sin for screen-Y).
Every existing `diamond_grip(id, w)` call becomes `center_grip(id,
w)` — the visual change is square-replacing-diamond at line
midpoints / arc midpoints / circle centres / ellipse centres /
viewport / hatch loop centroids / dimension grips / image corners /
multileader / ray / underlay / ole2frame / solid3d.
`LwPolyline` grows a stretch handle on every segment (straight or
arc), drawn as the new oriented rectangle. The grip-edit path
translates both segment endpoints for straight segments and
adjusts the arc bulge from the new midpoint for arc segments.
2026-05-29 02:07:34 +03:00
|
|
|
.map(|(grip_id, screen, _is_midpoint, shape, dir)| {
|
2026-03-22 22:44:32 +03:00
|
|
|
let is_hot = tab
|
|
|
|
|
.active_grip
|
|
|
|
|
.as_ref()
|
|
|
|
|
.map_or(false, |g| Some(g.handle) == sel_h && g.grip_id == grip_id);
|
2026-05-12 10:36:15 +03:00
|
|
|
overlay::GripMarker {
|
|
|
|
|
pos: screen,
|
|
|
|
|
shape,
|
|
|
|
|
is_hot,
|
refactor(grip): square-only marker set + oriented stretch handles
Phase 1 of bringing the grip vocabulary in line with the standard
CAD convention: drop the per-position-type shape encoding (Diamond
for translate, Circle for parameter) and use a plain square for
every endpoint / vertex / centre grip. The behavioural midpoint vs
vertex distinction stays — it lives on `GripDef::is_midpoint` and
drives `Translate` vs `Absolute` in the grip-edit dispatcher — only
the marker shape collapses.
- `GripShape` keeps `Square`, `Rectangle`, `Triangle`. Diamond /
Circle are gone.
- `GripDef` gains `dir: Option<[f32; 2]>` — a world-XY direction
vector consumed by `Rectangle` to orient the box along its
segment.
- `entities::common` exposes `square_grip`, `center_grip` (same
square marker, flagged as translate), `rectangle_grip(id, world,
dir)` for oriented mid-segment handles, and a kept-for-Phase-2
`triangle_grip`.
- `overlay::GripMarker` / `grips_to_screen[_paper]` thread `dir`
through to the canvas painter; the Rectangle path rotates the
box around the grip centre using the world-XY direction (flipping
the sin for screen-Y).
Every existing `diamond_grip(id, w)` call becomes `center_grip(id,
w)` — the visual change is square-replacing-diamond at line
midpoints / arc midpoints / circle centres / ellipse centres /
viewport / hatch loop centroids / dimension grips / image corners /
multileader / ray / underlay / ole2frame / solid3d.
`LwPolyline` grows a stretch handle on every segment (straight or
arc), drawn as the new oriented rectangle. The grip-edit path
translates both segment endpoints for straight segments and
adjusts the arc bulge from the new midpoint for arc segments.
2026-05-29 02:07:34 +03:00
|
|
|
dir,
|
2026-05-12 10:36:15 +03:00
|
|
|
}
|
2026-03-22 22:44:32 +03:00
|
|
|
})
|
|
|
|
|
.collect()
|
|
|
|
|
} else {
|
|
|
|
|
vec![]
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-03 19:25:55 +03:00
|
|
|
let (vw, vh) = tab.scene.selection.borrow().vp_size;
|
2026-05-28 09:45:19 +03:00
|
|
|
// Active tile rectangle (canvas-offset included) so grid / UCS
|
|
|
|
|
// icon / crosshair project through the active pane's camera at
|
|
|
|
|
// the correct place and scale.
|
|
|
|
|
let vp_bounds = tab.scene.active_model_tile_bounds(vw, vh);
|
2026-04-03 19:25:55 +03:00
|
|
|
|
2026-06-22 00:18:01 +03:00
|
|
|
// Each view draws its own grid, clipped to its own bounds, so they
|
|
|
|
|
// stay independent: model tiles in model space; the sheet plus each
|
fix(view): unify model VPORT / paper VIEWPORT view handling
Model-space tiled VPORTs and paper-space floating VIEWPORTs each carried
their own copy of the saved-view→camera math, so a fix to one drifted
from the other (the recurring grid/twist regressions). Route both, plus
the View-table and sheet-viewport paths, through one decoder and fix the
bugs that the duplication hid.
Decoder unification:
- New camera_from_view(direction, target, center, height, twist,
world_offset): the single saved-view→Camera decoder. camera_from_vport
(tiled), camera_for_viewport (floating, + auto-fit), and the View-entry
/ sheet-viewport apply paths all delegate to it. Tiled vs floating now
differ only in field source and the floating auto-fit fallback.
Twist round-trips on layout switch and file save:
- camera_from_view applies roll = -twist; the write path was dropping it,
so model→paper→model (and reload) reset the view to raw orientation.
- New Camera::roll() recovers the camera roll; sync_camera_to_document,
write_camera_view_entry, and vport_from_camera now store -roll as the
view twist on the *Active VPort, the OpenCADStudio_Camera_Model View
entry, the per-tile VPorts, and the paper sheet viewport.
Rotation-aware frustum cull:
- New view_cull_aabb(): the entity-cull rectangle is built from the
camera's right/up basis (so a twisted/rotated view's rotated rectangle
is enclosed) instead of world X/Y axes; returns None for tilted views
to disable the flat-XY cull rather than wrongly hide geometry. Applied
to the model-tile and paper-viewport cull sites.
Paper viewport scale:
- The floating-viewport auto-fit overlap test summed view_target +
view_center raw; under a twist view_center is a DCS offset, so the sum
landed far from the real WCS centre and wrongly tripped the auto-fit,
rendering content at the wrong zoom. It now tests the decoded
(twist-folded, wire-space) target. The CPU projection path derives its
scale from that same camera, locking hit-test/snap to the GPU render.
Grid enumeration:
- ViewportInstance gains grid_on; the two grid-view collectors collapse
into one grid_views() derived from the same active_viewports() list the
renderer uses, so the grid overlay can no longer drift from the views
on screen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 09:35:06 +03:00
|
|
|
// floating viewport (clipped to its rectangle) in paper space. One
|
|
|
|
|
// enumeration for both, shared with the renderer.
|
2026-06-22 10:03:35 +03:00
|
|
|
// Align the model grid to the active UCS (origin in wire space, world
|
|
|
|
|
// axis directions); paper space stays plain WCS.
|
2026-06-23 22:57:31 +03:00
|
|
|
let (grid_origin, grid_axes): (glam::DVec3, _) = if is_paper {
|
|
|
|
|
(glam::DVec3::ZERO, (glam::Vec3::X, glam::Vec3::Y, glam::Vec3::Z))
|
2026-06-22 10:03:35 +03:00
|
|
|
} else {
|
|
|
|
|
let (o, ux, uy, uz) = tab.ucs_xform().axes();
|
2026-06-23 22:57:31 +03:00
|
|
|
(o.as_dvec3(), (ux, uy, uz))
|
2026-06-22 10:03:35 +03:00
|
|
|
};
|
fix(view): unify model VPORT / paper VIEWPORT view handling
Model-space tiled VPORTs and paper-space floating VIEWPORTs each carried
their own copy of the saved-view→camera math, so a fix to one drifted
from the other (the recurring grid/twist regressions). Route both, plus
the View-table and sheet-viewport paths, through one decoder and fix the
bugs that the duplication hid.
Decoder unification:
- New camera_from_view(direction, target, center, height, twist,
world_offset): the single saved-view→Camera decoder. camera_from_vport
(tiled), camera_for_viewport (floating, + auto-fit), and the View-entry
/ sheet-viewport apply paths all delegate to it. Tiled vs floating now
differ only in field source and the floating auto-fit fallback.
Twist round-trips on layout switch and file save:
- camera_from_view applies roll = -twist; the write path was dropping it,
so model→paper→model (and reload) reset the view to raw orientation.
- New Camera::roll() recovers the camera roll; sync_camera_to_document,
write_camera_view_entry, and vport_from_camera now store -roll as the
view twist on the *Active VPort, the OpenCADStudio_Camera_Model View
entry, the per-tile VPorts, and the paper sheet viewport.
Rotation-aware frustum cull:
- New view_cull_aabb(): the entity-cull rectangle is built from the
camera's right/up basis (so a twisted/rotated view's rotated rectangle
is enclosed) instead of world X/Y axes; returns None for tilted views
to disable the flat-XY cull rather than wrongly hide geometry. Applied
to the model-tile and paper-viewport cull sites.
Paper viewport scale:
- The floating-viewport auto-fit overlap test summed view_target +
view_center raw; under a twist view_center is a DCS offset, so the sum
landed far from the real WCS centre and wrongly tripped the auto-fit,
rendering content at the wrong zoom. It now tests the decoded
(twist-folded, wire-space) target. The CPU projection path derives its
scale from that same camera, locking hit-test/snap to the GPU render.
Grid enumeration:
- ViewportInstance gains grid_on; the two grid-view collectors collapse
into one grid_views() derived from the same active_viewports() list the
renderer uses, so the grid overlay can no longer drift from the views
on screen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 09:35:06 +03:00
|
|
|
let grid: Vec<overlay::GridParams> = tab
|
|
|
|
|
.scene
|
|
|
|
|
.grid_views(vw, vh)
|
2026-06-22 00:18:01 +03:00
|
|
|
.into_iter()
|
|
|
|
|
.map(|(bounds, cam)| {
|
|
|
|
|
let plane = grid_plane_from_camera(cam.pitch, cam.yaw);
|
|
|
|
|
overlay::GridParams {
|
2026-06-23 22:57:31 +03:00
|
|
|
view_rot: cam.view_proj_rte(bounds),
|
|
|
|
|
eye: cam.eye_f64(),
|
2026-06-22 00:18:01 +03:00
|
|
|
bounds,
|
|
|
|
|
plane,
|
2026-06-22 10:03:35 +03:00
|
|
|
origin: grid_origin,
|
|
|
|
|
axes: grid_axes,
|
2026-06-22 00:18:01 +03:00
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
2026-04-03 19:25:55 +03:00
|
|
|
|
|
|
|
|
let ucs_icon = if self.show_ucs_icon && !is_paper {
|
|
|
|
|
let cam = tab.scene.camera.borrow();
|
feat(ucs): central WCS↔UCS converter; adopt file UCS, drive readout + icon
Introduce one bridge between WCS (how geometry and the file are stored)
and the active UCS (the coordinate system the user works in), so every
UCS-aware system routes through it instead of re-deriving axis math.
- UcsXform (app/helpers.rs): the converter — to_wcs/to_ucs (points),
vec_to_wcs/vec_to_ucs (directions), axes(), is_identity(). Orthonormal
axes, so the inverse is the transpose. The old ucs_to_wcs / ucs_rotate_vec
free fns now delegate to it (one implementation).
- DocumentTab::ucs_xform() builds it from the tab's active_ucs (None = WCS).
- DocumentTab::adopt_active_ucs_from_header() loads the document's saved
current UCS (header model-space UCS) into active_ucs on open, so the file's
coordinate system is live immediately; called from both load paths.
- Status-bar coordinate readout reports the cursor in the active UCS.
- UCS icon tripod projects the active UCS axes, so it rotates to the UCS
instead of always showing world X/Y/Z.
First slice of routing the app's coordinate systems through the converter;
input/picking, grid, ViewCube, and the remaining readouts follow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 09:57:39 +03:00
|
|
|
let (_, ux, uy, uz) = tab.ucs_xform().axes();
|
2026-04-03 19:25:55 +03:00
|
|
|
Some(overlay::UcsIconParams {
|
|
|
|
|
view_proj: cam.view_proj(vp_bounds),
|
|
|
|
|
bounds: vp_bounds,
|
feat(ucs): central WCS↔UCS converter; adopt file UCS, drive readout + icon
Introduce one bridge between WCS (how geometry and the file are stored)
and the active UCS (the coordinate system the user works in), so every
UCS-aware system routes through it instead of re-deriving axis math.
- UcsXform (app/helpers.rs): the converter — to_wcs/to_ucs (points),
vec_to_wcs/vec_to_ucs (directions), axes(), is_identity(). Orthonormal
axes, so the inverse is the transpose. The old ucs_to_wcs / ucs_rotate_vec
free fns now delegate to it (one implementation).
- DocumentTab::ucs_xform() builds it from the tab's active_ucs (None = WCS).
- DocumentTab::adopt_active_ucs_from_header() loads the document's saved
current UCS (header model-space UCS) into active_ucs on open, so the file's
coordinate system is live immediately; called from both load paths.
- Status-bar coordinate readout reports the cursor in the active UCS.
- UCS icon tripod projects the active UCS axes, so it rotates to the UCS
instead of always showing world X/Y/Z.
First slice of routing the app's coordinate systems through the converter;
input/picking, grid, ViewCube, and the remaining readouts follow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 09:57:39 +03:00
|
|
|
axes: (ux, uy, uz),
|
2026-04-03 19:25:55 +03:00
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-08 00:26:54 +03:00
|
|
|
// OST tracking points → screen positions.
|
|
|
|
|
let ost_points: Vec<overlay::OstTrackPoint> = if self.snapper.otrack_enabled {
|
|
|
|
|
let vp_mat = tab.scene.camera.borrow().view_proj(vp_bounds);
|
2026-05-12 10:36:15 +03:00
|
|
|
self.snapper
|
|
|
|
|
.tracking_points
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|&wp| {
|
|
|
|
|
let ndc = vp_mat.project_point3(wp);
|
|
|
|
|
overlay::OstTrackPoint {
|
|
|
|
|
screen: iced::Point::new(
|
|
|
|
|
(ndc.x + 1.0) * 0.5 * vp_bounds.width,
|
|
|
|
|
(1.0 - ndc.y) * 0.5 * vp_bounds.height,
|
|
|
|
|
),
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.collect()
|
2026-04-08 00:26:54 +03:00
|
|
|
} else {
|
|
|
|
|
vec![]
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-28 18:42:39 +03:00
|
|
|
// Model-space tile dividers (none in paper / single-tile layouts).
|
|
|
|
|
let tile_edges = if !is_paper {
|
|
|
|
|
tab.scene.model_tile_edges()
|
|
|
|
|
} else {
|
|
|
|
|
vec![]
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-12 10:36:15 +03:00
|
|
|
overlay::selection_overlay(
|
|
|
|
|
sel,
|
|
|
|
|
snap_info,
|
|
|
|
|
grips,
|
|
|
|
|
grid,
|
|
|
|
|
ucs_icon,
|
|
|
|
|
ost_points,
|
|
|
|
|
tab.last_cursor_screen,
|
|
|
|
|
!is_paper && self.show_viewcube,
|
2026-05-28 18:42:39 +03:00
|
|
|
tile_edges,
|
2026-05-12 10:36:15 +03:00
|
|
|
)
|
2026-03-22 22:44:32 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let viewport_mouse = mouse_area(container(
|
|
|
|
|
iced::widget::Space::new().width(Fill).height(Fill),
|
|
|
|
|
))
|
|
|
|
|
.on_move(Message::ViewportMove)
|
|
|
|
|
.on_press(Message::ViewportLeftPress)
|
|
|
|
|
.on_release(Message::ViewportLeftRelease)
|
|
|
|
|
.on_right_press(Message::ViewportRightPress)
|
|
|
|
|
.on_right_release(Message::ViewportRightRelease)
|
|
|
|
|
.on_middle_press(Message::ViewportMiddlePress)
|
|
|
|
|
.on_middle_release(Message::ViewportMiddleRelease)
|
|
|
|
|
.on_scroll(Message::ViewportScroll)
|
|
|
|
|
.on_exit(Message::ViewportExit);
|
|
|
|
|
|
|
|
|
|
let bg_color = if is_paper {
|
2026-04-23 14:05:07 +03:00
|
|
|
// Desk color — matches the DESK constant in paper_canvas.rs.
|
2026-05-12 10:36:15 +03:00
|
|
|
Color {
|
|
|
|
|
r: 0.22,
|
|
|
|
|
g: 0.24,
|
|
|
|
|
b: 0.28,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
}
|
2026-03-22 22:44:32 +03:00
|
|
|
} else {
|
2026-04-06 23:26:26 +03:00
|
|
|
tab.bg_color
|
|
|
|
|
.map(|[r, g, b, a]| Color { r, g, b, a })
|
2026-05-12 10:36:15 +03:00
|
|
|
.unwrap_or(Color {
|
|
|
|
|
r: 0.11,
|
|
|
|
|
g: 0.11,
|
|
|
|
|
b: 0.11,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
})
|
2026-03-22 22:44:32 +03:00
|
|
|
};
|
|
|
|
|
|
feat(dyn-input): TAB-editable distance/angle/coordinate fields
Dinamik giriş artık cursor yanında düzenlenebilir kutular gösteriyor.
TAB ile kutular arası geçilir, bir kutuya yazınca o kutu kilitlenir
(değer sabitlenir), diğerleri cursor'ı takip etmeye devam eder, Enter
ile çözülen nokta komuta point-pick olarak iletilir.
- DocumentTab.dyn_fields / dyn_active state; DynComponent + DynFieldEntry
- sync_dyn_fields komutun dyn_field() + base noktasına göre kutu kümesini
türetir: nokta+base → [d, açı], nokta+base yok → [X, Y], radius → [d],
açı → [açı]
- klavye yönlendirme: rakam/./-/+ aktif kutuya, TAB sonraki kutuya,
Enter try_dyn_commit, Backspace kilidi açar
- dyn_capturing iken command-line text alanı on_input'suz bırakılıyor
ki focus'u ve tuşları çalmasın; girişler global key aboneliğinden
geliyor (numpad dahil text alanı üzerinden yakalanıyor)
- çoklu-kutu overlay render: aktif kutu vurgulu, kilitli kutu farklı
kenar rengi
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:08:57 +03:00
|
|
|
// Dynamic input overlay — editable boxes near the cursor, one per
|
|
|
|
|
// quantity the active command is asking for (X/Y, or polar
|
|
|
|
|
// distance+angle, or a single distance/angle). TAB moves focus
|
|
|
|
|
// between boxes; typing locks a box to a fixed value while the
|
|
|
|
|
// rest keep tracking the cursor. The field set is maintained in
|
|
|
|
|
// `tab.dyn_fields` by `sync_dyn_fields`.
|
2026-06-13 23:58:56 +03:00
|
|
|
// A pick step (object selection) has no input box, but still shows
|
|
|
|
|
// its prompt ("Select first object …") near the cursor as a hint.
|
|
|
|
|
let dyn_picks_object = tab
|
|
|
|
|
.active_cmd
|
|
|
|
|
.as_ref()
|
|
|
|
|
.map(|c| c.needs_entity_pick() || c.needs_structure_point_pick())
|
|
|
|
|
.unwrap_or(false);
|
2026-04-08 00:12:09 +03:00
|
|
|
let dyn_input_overlay: Option<Element<'_, Message>> =
|
2026-06-13 23:58:56 +03:00
|
|
|
if self.dyn_input
|
|
|
|
|
&& tab.active_cmd.is_some()
|
|
|
|
|
&& (!tab.dyn_fields.is_empty() || dyn_picks_object)
|
|
|
|
|
{
|
2026-04-08 00:12:09 +03:00
|
|
|
let w = tab.last_cursor_world;
|
feat(dyn-input): TAB-editable distance/angle/coordinate fields
Dinamik giriş artık cursor yanında düzenlenebilir kutular gösteriyor.
TAB ile kutular arası geçilir, bir kutuya yazınca o kutu kilitlenir
(değer sabitlenir), diğerleri cursor'ı takip etmeye devam eder, Enter
ile çözülen nokta komuta point-pick olarak iletilir.
- DocumentTab.dyn_fields / dyn_active state; DynComponent + DynFieldEntry
- sync_dyn_fields komutun dyn_field() + base noktasına göre kutu kümesini
türetir: nokta+base → [d, açı], nokta+base yok → [X, Y], radius → [d],
açı → [açı]
- klavye yönlendirme: rakam/./-/+ aktif kutuya, TAB sonraki kutuya,
Enter try_dyn_commit, Backspace kilidi açar
- dyn_capturing iken command-line text alanı on_input'suz bırakılıyor
ki focus'u ve tuşları çalmasın; girişler global key aboneliğinden
geliyor (numpad dahil text alanı üzerinden yakalanıyor)
- çoklu-kutu overlay render: aktif kutu vurgulu, kilitli kutu farklı
kenar rengi
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:08:57 +03:00
|
|
|
let base = self.last_point;
|
2026-06-09 18:32:03 +03:00
|
|
|
// A command may drive a typed scalar by mouse (e.g. a
|
|
|
|
|
// perpendicular distance to a picked object); show that live
|
|
|
|
|
// value in the box until the user types over it.
|
|
|
|
|
let live = tab.active_cmd.as_ref().and_then(|c| c.dyn_live_value(w));
|
feat(dyn-input): TAB-editable distance/angle/coordinate fields
Dinamik giriş artık cursor yanında düzenlenebilir kutular gösteriyor.
TAB ile kutular arası geçilir, bir kutuya yazınca o kutu kilitlenir
(değer sabitlenir), diğerleri cursor'ı takip etmeye devam eder, Enter
ile çözülen nokta komuta point-pick olarak iletilir.
- DocumentTab.dyn_fields / dyn_active state; DynComponent + DynFieldEntry
- sync_dyn_fields komutun dyn_field() + base noktasına göre kutu kümesini
türetir: nokta+base → [d, açı], nokta+base yok → [X, Y], radius → [d],
açı → [açı]
- klavye yönlendirme: rakam/./-/+ aktif kutuya, TAB sonraki kutuya,
Enter try_dyn_commit, Backspace kilidi açar
- dyn_capturing iken command-line text alanı on_input'suz bırakılıyor
ki focus'u ve tuşları çalmasın; girişler global key aboneliğinden
geliyor (numpad dahil text alanı üzerinden yakalanıyor)
- çoklu-kutu overlay render: aktif kutu vurgulu, kilitli kutu farklı
kenar rengi
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:08:57 +03:00
|
|
|
let boxes: Vec<overlay::DynBox> = tab
|
|
|
|
|
.dyn_fields
|
|
|
|
|
.iter()
|
|
|
|
|
.enumerate()
|
2026-06-09 18:32:03 +03:00
|
|
|
.map(|(idx, f)| {
|
|
|
|
|
let value = match (&f.buffer, live) {
|
|
|
|
|
(Some(b), _) => b.clone(),
|
2026-06-15 22:03:04 +03:00
|
|
|
// An angle step with a command-supplied live value
|
|
|
|
|
// (ARC span / direction) shows it in degrees.
|
|
|
|
|
(None, Some(lv)) if f.component == DynComponent::Angle => {
|
|
|
|
|
format!("{lv:.1}")
|
|
|
|
|
}
|
2026-06-09 18:32:03 +03:00
|
|
|
(None, Some(lv))
|
|
|
|
|
if matches!(
|
|
|
|
|
f.component,
|
|
|
|
|
DynComponent::Scalar | DynComponent::Distance
|
|
|
|
|
) =>
|
|
|
|
|
{
|
|
|
|
|
format!("{lv:.4}")
|
|
|
|
|
}
|
2026-06-22 10:13:47 +03:00
|
|
|
_ => dyn_component_value(f, w, base, &tab.ucs_xform()),
|
2026-06-09 18:32:03 +03:00
|
|
|
};
|
|
|
|
|
overlay::DynBox {
|
2026-06-15 17:12:08 +03:00
|
|
|
label: f.role.label().to_string(),
|
2026-06-09 18:32:03 +03:00
|
|
|
value,
|
|
|
|
|
active: idx == tab.dyn_active,
|
|
|
|
|
locked: f.locked(),
|
2026-06-15 17:12:08 +03:00
|
|
|
role: f.role,
|
2026-06-09 18:32:03 +03:00
|
|
|
}
|
feat(dyn-input): TAB-editable distance/angle/coordinate fields
Dinamik giriş artık cursor yanında düzenlenebilir kutular gösteriyor.
TAB ile kutular arası geçilir, bir kutuya yazınca o kutu kilitlenir
(değer sabitlenir), diğerleri cursor'ı takip etmeye devam eder, Enter
ile çözülen nokta komuta point-pick olarak iletilir.
- DocumentTab.dyn_fields / dyn_active state; DynComponent + DynFieldEntry
- sync_dyn_fields komutun dyn_field() + base noktasına göre kutu kümesini
türetir: nokta+base → [d, açı], nokta+base yok → [X, Y], radius → [d],
açı → [açı]
- klavye yönlendirme: rakam/./-/+ aktif kutuya, TAB sonraki kutuya,
Enter try_dyn_commit, Backspace kilidi açar
- dyn_capturing iken command-line text alanı on_input'suz bırakılıyor
ki focus'u ve tuşları çalmasın; girişler global key aboneliğinden
geliyor (numpad dahil text alanı üzerinden yakalanıyor)
- çoklu-kutu overlay render: aktif kutu vurgulu, kilitli kutu farklı
kenar rengi
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:08:57 +03:00
|
|
|
})
|
|
|
|
|
.collect();
|
2026-06-09 18:32:03 +03:00
|
|
|
let prompt = tab
|
|
|
|
|
.active_cmd
|
|
|
|
|
.as_ref()
|
|
|
|
|
.map(|c| c.prompt())
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
Some(overlay::dynamic_input_overlay(
|
|
|
|
|
tab.last_cursor_screen,
|
2026-06-15 17:12:08 +03:00
|
|
|
tab.last_point_screen,
|
2026-06-15 18:46:34 +03:00
|
|
|
tab.dyn_ref_screen,
|
2026-06-15 17:12:08 +03:00
|
|
|
tab.dyn_guide,
|
2026-06-09 18:32:03 +03:00
|
|
|
boxes,
|
|
|
|
|
prompt,
|
|
|
|
|
))
|
2026-04-08 00:12:09 +03:00
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
};
|
|
|
|
|
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
let mut viewport_stack = if tab.is_start {
|
|
|
|
|
// Start tab: only the welcome widget over a flat background.
|
|
|
|
|
// Skip every drawing-only overlay (selection markers, snap info,
|
|
|
|
|
// mouse-area capturing draw clicks, viewcube, nav toolbar, …).
|
|
|
|
|
stack![container(viewport_3d)
|
2026-03-22 22:44:32 +03:00
|
|
|
.style(move |_: &Theme| container::Style {
|
|
|
|
|
background: Some(Background::Color(bg_color)),
|
|
|
|
|
..Default::default()
|
|
|
|
|
})
|
|
|
|
|
.width(Fill)
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
.height(Fill)]
|
|
|
|
|
.width(Fill)
|
|
|
|
|
.height(Fill)
|
2026-05-28 10:03:01 +03:00
|
|
|
} else if is_paper {
|
2026-06-02 20:25:50 +03:00
|
|
|
// Paper layout: the GPU shader renders everything — the desk is the
|
|
|
|
|
// container background, the white sheet + paper entities + borders
|
|
|
|
|
// come from the full-canvas top-locked "sheet" viewport, and the
|
|
|
|
|
// floating content viewports overlay it (same path as model space).
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
const DESK: Color = Color {
|
|
|
|
|
r: 0.22,
|
|
|
|
|
g: 0.24,
|
|
|
|
|
b: 0.28,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
2026-05-28 10:03:01 +03:00
|
|
|
stack![
|
2026-06-02 20:25:50 +03:00
|
|
|
container(viewport_3d)
|
|
|
|
|
.style(move |_: &Theme| container::Style {
|
|
|
|
|
background: Some(Background::Color(DESK)),
|
|
|
|
|
..Default::default()
|
|
|
|
|
})
|
|
|
|
|
.width(Fill)
|
|
|
|
|
.height(Fill),
|
2026-05-28 10:03:01 +03:00
|
|
|
selection_overlay,
|
|
|
|
|
viewport_mouse,
|
|
|
|
|
]
|
|
|
|
|
.width(Fill)
|
|
|
|
|
.height(Fill)
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
} else {
|
|
|
|
|
stack![
|
|
|
|
|
container(viewport_3d)
|
|
|
|
|
.style(move |_: &Theme| container::Style {
|
|
|
|
|
background: Some(Background::Color(bg_color)),
|
|
|
|
|
..Default::default()
|
|
|
|
|
})
|
|
|
|
|
.width(Fill)
|
|
|
|
|
.height(Fill),
|
|
|
|
|
selection_overlay,
|
|
|
|
|
viewport_mouse,
|
2026-05-27 18:17:10 +03:00
|
|
|
]
|
|
|
|
|
.width(Fill)
|
|
|
|
|
.height(Fill)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Model-space render-mode picker, top-left. Sits ABOVE the
|
|
|
|
|
// viewport mouse_area so clicks inside its bounds reach it
|
|
|
|
|
// instead of the shader behind it; `opaque` stops them bubbling
|
|
|
|
|
// further. Outside the chip the Fill container is transparent so
|
|
|
|
|
// viewport drawing / selection is unaffected. In a paper layout
|
|
|
|
|
// the active viewport gets its own picker (below) instead.
|
|
|
|
|
if !is_paper && !tab.is_start {
|
2026-06-18 23:29:34 +03:00
|
|
|
// Unified control chip: split buttons + render-mode picker +
|
|
|
|
|
// grid / grid-snap toggles, for the active Model tile.
|
|
|
|
|
let bar = viewport_controls(
|
|
|
|
|
tab.render_mode,
|
|
|
|
|
self.show_grid,
|
|
|
|
|
self.snapper.is_on(crate::snap::SnapType::Grid),
|
|
|
|
|
true,
|
|
|
|
|
);
|
2026-05-28 09:45:19 +03:00
|
|
|
// Position the bar at the active model tile's top-left corner so
|
|
|
|
|
// it follows the active panel in a tiled layout (full canvas when
|
|
|
|
|
// a single tile fills the window). Leading Spaces offset it.
|
|
|
|
|
let (vw, vh) = tab.scene.selection.borrow().vp_size;
|
|
|
|
|
let rect = tab.scene.active_model_tile_bounds(vw, vh);
|
|
|
|
|
let bar_layer = column![
|
|
|
|
|
Space::new().height(iced::Length::Fixed(rect.y.max(0.0))),
|
|
|
|
|
row![
|
|
|
|
|
Space::new().width(iced::Length::Fixed(rect.x.max(0.0))),
|
|
|
|
|
iced::widget::opaque(bar),
|
|
|
|
|
],
|
|
|
|
|
]
|
|
|
|
|
.width(Fill)
|
|
|
|
|
.height(Fill);
|
|
|
|
|
viewport_stack = viewport_stack.push(bar_layer);
|
2026-05-27 18:17:10 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Active paper-space viewport overlays: a render-mode picker in
|
|
|
|
|
// its top-left corner and a ViewCube hit area in its top-right,
|
|
|
|
|
// both layered ABOVE the viewport mouse_area so they receive
|
2026-06-02 20:33:51 +03:00
|
|
|
// clicks (the shader viewport sits below it). Positioned with
|
2026-05-27 18:17:10 +03:00
|
|
|
// leading Spaces sized to the viewport's screen rectangle.
|
|
|
|
|
let active_vp_rect: Option<iced::Rectangle> = if is_paper && !tab.is_start {
|
|
|
|
|
tab.scene.active_viewport.and_then(|h| {
|
|
|
|
|
let (cw, ch) = tab.scene.selection.borrow().vp_size;
|
|
|
|
|
tab.scene.viewport_screen_rect(h, (cw, ch))
|
|
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
};
|
|
|
|
|
if let Some(rect) = active_vp_rect {
|
|
|
|
|
let x = rect.x.max(0.0);
|
|
|
|
|
let y = rect.y.max(0.0);
|
2026-05-28 10:03:01 +03:00
|
|
|
// Highlight the active viewport with a 2-px border so its
|
|
|
|
|
// boundary is always visible over the GPU shader.
|
|
|
|
|
const VP_BORDER: Color = Color {
|
|
|
|
|
r: 0.18,
|
|
|
|
|
g: 0.52,
|
|
|
|
|
b: 0.95,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
|
|
|
|
let border_frame = container(
|
|
|
|
|
Space::new()
|
|
|
|
|
.width(iced::Length::Fixed(rect.width.max(1.0)))
|
|
|
|
|
.height(iced::Length::Fixed(rect.height.max(1.0))),
|
|
|
|
|
)
|
|
|
|
|
.style(move |_: &Theme| container::Style {
|
|
|
|
|
border: iced::Border {
|
|
|
|
|
color: VP_BORDER,
|
|
|
|
|
width: 2.0,
|
|
|
|
|
radius: 0.0.into(),
|
|
|
|
|
},
|
|
|
|
|
..Default::default()
|
|
|
|
|
});
|
|
|
|
|
let border_layer = column![
|
|
|
|
|
Space::new().height(iced::Length::Fixed(y)),
|
|
|
|
|
row![Space::new().width(iced::Length::Fixed(x)), border_frame,],
|
|
|
|
|
]
|
|
|
|
|
.width(Fill)
|
|
|
|
|
.height(Fill);
|
|
|
|
|
viewport_stack = viewport_stack.push(border_layer);
|
|
|
|
|
|
2026-05-27 18:17:10 +03:00
|
|
|
let vp_mode = tab
|
|
|
|
|
.scene
|
|
|
|
|
.active_viewport_render_mode()
|
|
|
|
|
.unwrap_or(acadrust::entities::ViewportRenderMode::Wireframe2D);
|
|
|
|
|
let picker_layer = column![
|
|
|
|
|
Space::new().height(iced::Length::Fixed(y + 4.0)),
|
|
|
|
|
row![
|
|
|
|
|
Space::new().width(iced::Length::Fixed(x + 4.0)),
|
2026-06-18 23:29:34 +03:00
|
|
|
iced::widget::opaque(viewport_controls(
|
|
|
|
|
vp_mode,
|
|
|
|
|
self.show_grid,
|
|
|
|
|
self.snapper.is_on(crate::snap::SnapType::Grid),
|
|
|
|
|
false,
|
|
|
|
|
)),
|
2026-05-27 18:17:10 +03:00
|
|
|
],
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
]
|
|
|
|
|
.width(Fill)
|
2026-05-27 18:17:10 +03:00
|
|
|
.height(Fill);
|
|
|
|
|
viewport_stack = viewport_stack.push(picker_layer);
|
|
|
|
|
|
|
|
|
|
if self.show_viewcube {
|
|
|
|
|
let cube_x = (rect.x + rect.width - VIEWCUBE_HIT_SIZE - VIEWCUBE_PAD).max(0.0);
|
|
|
|
|
let cube_y = (rect.y + VIEWCUBE_PAD).max(0.0);
|
|
|
|
|
let cube_click = column![
|
|
|
|
|
Space::new().height(iced::Length::Fixed(cube_y)),
|
|
|
|
|
row![
|
|
|
|
|
Space::new().width(iced::Length::Fixed(cube_x)),
|
|
|
|
|
mouse_area(
|
|
|
|
|
iced::widget::Space::new()
|
|
|
|
|
.width(iced::Length::Fixed(VIEWCUBE_HIT_SIZE))
|
|
|
|
|
.height(iced::Length::Fixed(VIEWCUBE_HIT_SIZE)),
|
|
|
|
|
)
|
|
|
|
|
.on_move(Message::CursorMoved)
|
|
|
|
|
.on_press(Message::ViewportClick),
|
|
|
|
|
],
|
|
|
|
|
]
|
|
|
|
|
.width(Fill)
|
|
|
|
|
.height(Fill);
|
|
|
|
|
viewport_stack = viewport_stack.push(cube_click);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-24 01:23:06 +03:00
|
|
|
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
if self.show_viewcube && !is_paper && !tab.is_start {
|
2026-05-28 09:45:19 +03:00
|
|
|
// Place the ViewCube hit area in the active model tile's top-right
|
|
|
|
|
// corner so it tracks the active panel in a tiled layout. The hit
|
|
|
|
|
// test in update.rs already maps clicks through the active tile.
|
|
|
|
|
let (vw, vh) = tab.scene.selection.borrow().vp_size;
|
|
|
|
|
let rect = tab.scene.active_model_tile_bounds(vw, vh);
|
|
|
|
|
let cube_x = (rect.x + rect.width - VIEWCUBE_HIT_SIZE - VIEWCUBE_PAD).max(0.0);
|
|
|
|
|
let cube_y = (rect.y + VIEWCUBE_PAD).max(0.0);
|
|
|
|
|
let cube_click = column![
|
|
|
|
|
Space::new().height(iced::Length::Fixed(cube_y)),
|
|
|
|
|
row![
|
|
|
|
|
Space::new().width(iced::Length::Fixed(cube_x)),
|
|
|
|
|
mouse_area(
|
|
|
|
|
iced::widget::Space::new()
|
|
|
|
|
.width(iced::Length::Fixed(VIEWCUBE_HIT_SIZE))
|
|
|
|
|
.height(iced::Length::Fixed(VIEWCUBE_HIT_SIZE)),
|
|
|
|
|
)
|
|
|
|
|
.on_move(Message::CursorMoved)
|
|
|
|
|
.on_press(Message::ViewportClick),
|
|
|
|
|
],
|
|
|
|
|
]
|
2026-04-24 01:23:06 +03:00
|
|
|
.width(Fill)
|
2026-05-28 09:45:19 +03:00
|
|
|
.height(Fill);
|
2026-04-24 01:23:06 +03:00
|
|
|
viewport_stack = viewport_stack.push(cube_click);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-08 00:12:09 +03:00
|
|
|
if let Some(dyn_ol) = dyn_input_overlay {
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
if !tab.is_start {
|
|
|
|
|
viewport_stack = viewport_stack.push(dyn_ol);
|
|
|
|
|
}
|
2026-04-08 00:12:09 +03:00
|
|
|
}
|
2026-03-22 22:44:32 +03:00
|
|
|
|
2026-05-29 09:51:12 +03:00
|
|
|
// Multi-functional grip popup (Phase 2). One bordered container
|
|
|
|
|
// wraps a column of borderless item buttons so the popup reads
|
|
|
|
|
// as a single widget instead of stacked tiles.
|
feat(grip): multi-functional hover popup menu
Phase 2 of the grip overhaul: when the cursor dwells on a selected
entity's grip the overlay opens a popup menu with entity-specific
options (Stretch / Add Vertex / Remove Vertex / Convert to
Arc-Line / …). Picking an item dispatches `apply_grip_menu`; the
default `Stretch` entry is a no-op that lets the user click the
grip to start the normal drag.
- `GripMenuItem` + `GripMenuAction` (full vocabulary covering
stretch, add/remove vertex, convert to arc/line, reverse arrows,
text-position variants, hatch parameters, tangent direction).
- `Grippable` trait gains `grip_menu` + `apply_grip_menu` with
trait-default `Stretch`-only impls so every existing entity gets
the popup for free.
- `EntityTypeOps` dispatches both methods across the same entity
list as the existing grip / property machinery.
- `OpenCADStudio` carries `grip_hover` (dwell tracker) +
`grip_popup` (open menu). `update_grip_hover` runs from
`ViewportMove`: hit-tests grips, refreshes the dwell timer, and
opens the popup after 600 ms. Cursor drift past 80 px or a grip
click dismisses it.
- `view.rs` paints the popup as an absolute-positioned column of
buttons at the grip's screen anchor; each button publishes
`Message::GripMenuPick(idx)`.
- `LwPolyline` overrides `Grippable` manually (skips the standard
macro) so its vertex grip exposes Add/Remove Vertex and its
segment-midpoint grip exposes Add Vertex + Convert to Arc/Line.
`apply_grip_menu` performs the edits: vertex insertion at the
chord midpoint inheriting the previous bulge, vertex removal,
and bulge toggle for the arc/line conversion.
2026-05-29 09:30:54 +03:00
|
|
|
if let Some(popup) = self.grip_popup.as_ref() {
|
|
|
|
|
if !tab.is_start {
|
2026-05-29 10:00:30 +03:00
|
|
|
// Size the row to the widest label so the selection
|
|
|
|
|
// highlight fills the whole row instead of just the
|
|
|
|
|
// text glyphs. ~7 px per character at size 12 + the
|
|
|
|
|
// horizontal padding (10 + 10).
|
|
|
|
|
let max_len = popup
|
|
|
|
|
.items
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|i| i.label.chars().count())
|
|
|
|
|
.max()
|
|
|
|
|
.unwrap_or(8) as f32;
|
|
|
|
|
let row_w = max_len * 7.0 + 24.0;
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
let mut col = column![].spacing(0).width(iced::Length::Fixed(row_w));
|
feat(grip): multi-functional hover popup menu
Phase 2 of the grip overhaul: when the cursor dwells on a selected
entity's grip the overlay opens a popup menu with entity-specific
options (Stretch / Add Vertex / Remove Vertex / Convert to
Arc-Line / …). Picking an item dispatches `apply_grip_menu`; the
default `Stretch` entry is a no-op that lets the user click the
grip to start the normal drag.
- `GripMenuItem` + `GripMenuAction` (full vocabulary covering
stretch, add/remove vertex, convert to arc/line, reverse arrows,
text-position variants, hatch parameters, tangent direction).
- `Grippable` trait gains `grip_menu` + `apply_grip_menu` with
trait-default `Stretch`-only impls so every existing entity gets
the popup for free.
- `EntityTypeOps` dispatches both methods across the same entity
list as the existing grip / property machinery.
- `OpenCADStudio` carries `grip_hover` (dwell tracker) +
`grip_popup` (open menu). `update_grip_hover` runs from
`ViewportMove`: hit-tests grips, refreshes the dwell timer, and
opens the popup after 600 ms. Cursor drift past 80 px or a grip
click dismisses it.
- `view.rs` paints the popup as an absolute-positioned column of
buttons at the grip's screen anchor; each button publishes
`Message::GripMenuPick(idx)`.
- `LwPolyline` overrides `Grippable` manually (skips the standard
macro) so its vertex grip exposes Add/Remove Vertex and its
segment-midpoint grip exposes Add Vertex + Convert to Arc/Line.
`apply_grip_menu` performs the edits: vertex insertion at the
chord midpoint inheriting the previous bulge, vertex removal,
and bulge toggle for the arc/line conversion.
2026-05-29 09:30:54 +03:00
|
|
|
for (idx, item) in popup.items.iter().enumerate() {
|
|
|
|
|
let is_sel = idx == popup.selected;
|
|
|
|
|
let label = item.label;
|
|
|
|
|
let btn = button(text(label).size(12).color(Color::WHITE))
|
|
|
|
|
.on_press(Message::GripMenuPick(idx))
|
2026-05-29 09:51:12 +03:00
|
|
|
.padding([3, 10])
|
|
|
|
|
.width(Fill)
|
feat(grip): multi-functional hover popup menu
Phase 2 of the grip overhaul: when the cursor dwells on a selected
entity's grip the overlay opens a popup menu with entity-specific
options (Stretch / Add Vertex / Remove Vertex / Convert to
Arc-Line / …). Picking an item dispatches `apply_grip_menu`; the
default `Stretch` entry is a no-op that lets the user click the
grip to start the normal drag.
- `GripMenuItem` + `GripMenuAction` (full vocabulary covering
stretch, add/remove vertex, convert to arc/line, reverse arrows,
text-position variants, hatch parameters, tangent direction).
- `Grippable` trait gains `grip_menu` + `apply_grip_menu` with
trait-default `Stretch`-only impls so every existing entity gets
the popup for free.
- `EntityTypeOps` dispatches both methods across the same entity
list as the existing grip / property machinery.
- `OpenCADStudio` carries `grip_hover` (dwell tracker) +
`grip_popup` (open menu). `update_grip_hover` runs from
`ViewportMove`: hit-tests grips, refreshes the dwell timer, and
opens the popup after 600 ms. Cursor drift past 80 px or a grip
click dismisses it.
- `view.rs` paints the popup as an absolute-positioned column of
buttons at the grip's screen anchor; each button publishes
`Message::GripMenuPick(idx)`.
- `LwPolyline` overrides `Grippable` manually (skips the standard
macro) so its vertex grip exposes Add/Remove Vertex and its
segment-midpoint grip exposes Add Vertex + Convert to Arc/Line.
`apply_grip_menu` performs the edits: vertex insertion at the
chord midpoint inheriting the previous bulge, vertex removal,
and bulge toggle for the arc/line conversion.
2026-05-29 09:30:54 +03:00
|
|
|
.style(move |_: &Theme, status| iced::widget::button::Style {
|
|
|
|
|
background: Some(Background::Color(match (is_sel, status) {
|
|
|
|
|
(true, _) => Color {
|
|
|
|
|
r: 0.20,
|
|
|
|
|
g: 0.45,
|
|
|
|
|
b: 0.95,
|
2026-05-29 09:51:12 +03:00
|
|
|
a: 1.0,
|
feat(grip): multi-functional hover popup menu
Phase 2 of the grip overhaul: when the cursor dwells on a selected
entity's grip the overlay opens a popup menu with entity-specific
options (Stretch / Add Vertex / Remove Vertex / Convert to
Arc-Line / …). Picking an item dispatches `apply_grip_menu`; the
default `Stretch` entry is a no-op that lets the user click the
grip to start the normal drag.
- `GripMenuItem` + `GripMenuAction` (full vocabulary covering
stretch, add/remove vertex, convert to arc/line, reverse arrows,
text-position variants, hatch parameters, tangent direction).
- `Grippable` trait gains `grip_menu` + `apply_grip_menu` with
trait-default `Stretch`-only impls so every existing entity gets
the popup for free.
- `EntityTypeOps` dispatches both methods across the same entity
list as the existing grip / property machinery.
- `OpenCADStudio` carries `grip_hover` (dwell tracker) +
`grip_popup` (open menu). `update_grip_hover` runs from
`ViewportMove`: hit-tests grips, refreshes the dwell timer, and
opens the popup after 600 ms. Cursor drift past 80 px or a grip
click dismisses it.
- `view.rs` paints the popup as an absolute-positioned column of
buttons at the grip's screen anchor; each button publishes
`Message::GripMenuPick(idx)`.
- `LwPolyline` overrides `Grippable` manually (skips the standard
macro) so its vertex grip exposes Add/Remove Vertex and its
segment-midpoint grip exposes Add Vertex + Convert to Arc/Line.
`apply_grip_menu` performs the edits: vertex insertion at the
chord midpoint inheriting the previous bulge, vertex removal,
and bulge toggle for the arc/line conversion.
2026-05-29 09:30:54 +03:00
|
|
|
},
|
|
|
|
|
(_, iced::widget::button::Status::Hovered) => Color {
|
2026-05-29 09:51:12 +03:00
|
|
|
r: 0.22,
|
|
|
|
|
g: 0.22,
|
|
|
|
|
b: 0.22,
|
|
|
|
|
a: 1.0,
|
feat(grip): multi-functional hover popup menu
Phase 2 of the grip overhaul: when the cursor dwells on a selected
entity's grip the overlay opens a popup menu with entity-specific
options (Stretch / Add Vertex / Remove Vertex / Convert to
Arc-Line / …). Picking an item dispatches `apply_grip_menu`; the
default `Stretch` entry is a no-op that lets the user click the
grip to start the normal drag.
- `GripMenuItem` + `GripMenuAction` (full vocabulary covering
stretch, add/remove vertex, convert to arc/line, reverse arrows,
text-position variants, hatch parameters, tangent direction).
- `Grippable` trait gains `grip_menu` + `apply_grip_menu` with
trait-default `Stretch`-only impls so every existing entity gets
the popup for free.
- `EntityTypeOps` dispatches both methods across the same entity
list as the existing grip / property machinery.
- `OpenCADStudio` carries `grip_hover` (dwell tracker) +
`grip_popup` (open menu). `update_grip_hover` runs from
`ViewportMove`: hit-tests grips, refreshes the dwell timer, and
opens the popup after 600 ms. Cursor drift past 80 px or a grip
click dismisses it.
- `view.rs` paints the popup as an absolute-positioned column of
buttons at the grip's screen anchor; each button publishes
`Message::GripMenuPick(idx)`.
- `LwPolyline` overrides `Grippable` manually (skips the standard
macro) so its vertex grip exposes Add/Remove Vertex and its
segment-midpoint grip exposes Add Vertex + Convert to Arc/Line.
`apply_grip_menu` performs the edits: vertex insertion at the
chord midpoint inheriting the previous bulge, vertex removal,
and bulge toggle for the arc/line conversion.
2026-05-29 09:30:54 +03:00
|
|
|
},
|
2026-05-29 09:51:12 +03:00
|
|
|
_ => Color::TRANSPARENT,
|
feat(grip): multi-functional hover popup menu
Phase 2 of the grip overhaul: when the cursor dwells on a selected
entity's grip the overlay opens a popup menu with entity-specific
options (Stretch / Add Vertex / Remove Vertex / Convert to
Arc-Line / …). Picking an item dispatches `apply_grip_menu`; the
default `Stretch` entry is a no-op that lets the user click the
grip to start the normal drag.
- `GripMenuItem` + `GripMenuAction` (full vocabulary covering
stretch, add/remove vertex, convert to arc/line, reverse arrows,
text-position variants, hatch parameters, tangent direction).
- `Grippable` trait gains `grip_menu` + `apply_grip_menu` with
trait-default `Stretch`-only impls so every existing entity gets
the popup for free.
- `EntityTypeOps` dispatches both methods across the same entity
list as the existing grip / property machinery.
- `OpenCADStudio` carries `grip_hover` (dwell tracker) +
`grip_popup` (open menu). `update_grip_hover` runs from
`ViewportMove`: hit-tests grips, refreshes the dwell timer, and
opens the popup after 600 ms. Cursor drift past 80 px or a grip
click dismisses it.
- `view.rs` paints the popup as an absolute-positioned column of
buttons at the grip's screen anchor; each button publishes
`Message::GripMenuPick(idx)`.
- `LwPolyline` overrides `Grippable` manually (skips the standard
macro) so its vertex grip exposes Add/Remove Vertex and its
segment-midpoint grip exposes Add Vertex + Convert to Arc/Line.
`apply_grip_menu` performs the edits: vertex insertion at the
chord midpoint inheriting the previous bulge, vertex removal,
and bulge toggle for the arc/line conversion.
2026-05-29 09:30:54 +03:00
|
|
|
})),
|
|
|
|
|
border: Border {
|
2026-05-29 09:51:12 +03:00
|
|
|
color: Color::TRANSPARENT,
|
|
|
|
|
width: 0.0,
|
feat(grip): multi-functional hover popup menu
Phase 2 of the grip overhaul: when the cursor dwells on a selected
entity's grip the overlay opens a popup menu with entity-specific
options (Stretch / Add Vertex / Remove Vertex / Convert to
Arc-Line / …). Picking an item dispatches `apply_grip_menu`; the
default `Stretch` entry is a no-op that lets the user click the
grip to start the normal drag.
- `GripMenuItem` + `GripMenuAction` (full vocabulary covering
stretch, add/remove vertex, convert to arc/line, reverse arrows,
text-position variants, hatch parameters, tangent direction).
- `Grippable` trait gains `grip_menu` + `apply_grip_menu` with
trait-default `Stretch`-only impls so every existing entity gets
the popup for free.
- `EntityTypeOps` dispatches both methods across the same entity
list as the existing grip / property machinery.
- `OpenCADStudio` carries `grip_hover` (dwell tracker) +
`grip_popup` (open menu). `update_grip_hover` runs from
`ViewportMove`: hit-tests grips, refreshes the dwell timer, and
opens the popup after 600 ms. Cursor drift past 80 px or a grip
click dismisses it.
- `view.rs` paints the popup as an absolute-positioned column of
buttons at the grip's screen anchor; each button publishes
`Message::GripMenuPick(idx)`.
- `LwPolyline` overrides `Grippable` manually (skips the standard
macro) so its vertex grip exposes Add/Remove Vertex and its
segment-midpoint grip exposes Add Vertex + Convert to Arc/Line.
`apply_grip_menu` performs the edits: vertex insertion at the
chord midpoint inheriting the previous bulge, vertex removal,
and bulge toggle for the arc/line conversion.
2026-05-29 09:30:54 +03:00
|
|
|
radius: 0.0.into(),
|
|
|
|
|
},
|
|
|
|
|
text_color: Color::WHITE,
|
|
|
|
|
..Default::default()
|
|
|
|
|
});
|
2026-05-29 09:51:12 +03:00
|
|
|
col = col.push(btn);
|
feat(grip): multi-functional hover popup menu
Phase 2 of the grip overhaul: when the cursor dwells on a selected
entity's grip the overlay opens a popup menu with entity-specific
options (Stretch / Add Vertex / Remove Vertex / Convert to
Arc-Line / …). Picking an item dispatches `apply_grip_menu`; the
default `Stretch` entry is a no-op that lets the user click the
grip to start the normal drag.
- `GripMenuItem` + `GripMenuAction` (full vocabulary covering
stretch, add/remove vertex, convert to arc/line, reverse arrows,
text-position variants, hatch parameters, tangent direction).
- `Grippable` trait gains `grip_menu` + `apply_grip_menu` with
trait-default `Stretch`-only impls so every existing entity gets
the popup for free.
- `EntityTypeOps` dispatches both methods across the same entity
list as the existing grip / property machinery.
- `OpenCADStudio` carries `grip_hover` (dwell tracker) +
`grip_popup` (open menu). `update_grip_hover` runs from
`ViewportMove`: hit-tests grips, refreshes the dwell timer, and
opens the popup after 600 ms. Cursor drift past 80 px or a grip
click dismisses it.
- `view.rs` paints the popup as an absolute-positioned column of
buttons at the grip's screen anchor; each button publishes
`Message::GripMenuPick(idx)`.
- `LwPolyline` overrides `Grippable` manually (skips the standard
macro) so its vertex grip exposes Add/Remove Vertex and its
segment-midpoint grip exposes Add Vertex + Convert to Arc/Line.
`apply_grip_menu` performs the edits: vertex insertion at the
chord midpoint inheriting the previous bulge, vertex removal,
and bulge toggle for the arc/line conversion.
2026-05-29 09:30:54 +03:00
|
|
|
}
|
2026-05-29 09:51:12 +03:00
|
|
|
let menu_panel = container(col)
|
|
|
|
|
.padding(2)
|
|
|
|
|
.style(|_: &Theme| container::Style {
|
|
|
|
|
background: Some(Background::Color(Color {
|
|
|
|
|
r: 0.10,
|
|
|
|
|
g: 0.10,
|
|
|
|
|
b: 0.10,
|
|
|
|
|
a: 0.95,
|
|
|
|
|
})),
|
|
|
|
|
border: Border {
|
|
|
|
|
color: Color {
|
|
|
|
|
r: 0.40,
|
|
|
|
|
g: 0.40,
|
|
|
|
|
b: 0.40,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
},
|
|
|
|
|
width: 1.0,
|
|
|
|
|
radius: 3.0.into(),
|
|
|
|
|
},
|
|
|
|
|
..Default::default()
|
|
|
|
|
});
|
2026-05-29 15:58:23 +03:00
|
|
|
// Offset the menu by 12 px so the cursor doesn't land on
|
|
|
|
|
// the first item immediately, matching the right-click
|
|
|
|
|
// context menu's "panel below the click point" feel.
|
|
|
|
|
let anchor = iced::Point::new(popup.anchor.x + 12.0, popup.anchor.y + 12.0);
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
viewport_stack =
|
|
|
|
|
viewport_stack.push(position_canvas_overlay(anchor, menu_panel.into()));
|
2026-05-29 15:58:23 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-11 20:11:19 +03:00
|
|
|
// Dynamic-block visibility-state dropdown.
|
|
|
|
|
if let Some(popup) = self.visibility_popup.as_ref() {
|
|
|
|
|
if !tab.is_start {
|
|
|
|
|
let max_len = popup
|
|
|
|
|
.items
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|s| s.chars().count())
|
|
|
|
|
.max()
|
|
|
|
|
.unwrap_or(4) as f32;
|
|
|
|
|
// +2 chars for the leading "✓ " / " " marker column.
|
|
|
|
|
let row_w = (max_len + 2.0) * 7.0 + 24.0;
|
|
|
|
|
let mut col = column![].spacing(0).width(iced::Length::Fixed(row_w));
|
|
|
|
|
for (idx, name) in popup.items.iter().enumerate() {
|
|
|
|
|
let is_cur = popup.current == Some(idx);
|
2026-06-18 12:29:32 +03:00
|
|
|
let mark: Element<'_, Message> = if is_cur {
|
|
|
|
|
crate::ui::icons::tinted(crate::ui::icons::CHECK, 11.0, Color::WHITE)
|
|
|
|
|
} else {
|
|
|
|
|
Space::new().width(11).into()
|
|
|
|
|
};
|
|
|
|
|
let btn = button(
|
|
|
|
|
row![
|
|
|
|
|
container(mark).width(16),
|
|
|
|
|
text(name).size(12).color(Color::WHITE),
|
|
|
|
|
]
|
|
|
|
|
.spacing(2)
|
|
|
|
|
.align_y(iced::Center),
|
|
|
|
|
)
|
|
|
|
|
.on_press(Message::VisibilityPick(idx))
|
2026-06-11 20:11:19 +03:00
|
|
|
.padding([3, 10])
|
|
|
|
|
.width(Fill)
|
|
|
|
|
.style(move |_: &Theme, status| iced::widget::button::Style {
|
|
|
|
|
background: Some(Background::Color(match status {
|
|
|
|
|
iced::widget::button::Status::Hovered => Color {
|
|
|
|
|
r: 0.20,
|
|
|
|
|
g: 0.45,
|
|
|
|
|
b: 0.95,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
},
|
|
|
|
|
_ => Color::TRANSPARENT,
|
|
|
|
|
})),
|
|
|
|
|
border: Border {
|
|
|
|
|
color: Color::TRANSPARENT,
|
|
|
|
|
width: 0.0,
|
|
|
|
|
radius: 0.0.into(),
|
|
|
|
|
},
|
|
|
|
|
text_color: Color::WHITE,
|
|
|
|
|
..Default::default()
|
|
|
|
|
});
|
|
|
|
|
col = col.push(btn);
|
|
|
|
|
}
|
|
|
|
|
let panel = container(iced::widget::scrollable(col).height(iced::Length::Shrink))
|
|
|
|
|
.max_height(360.0)
|
|
|
|
|
.padding(2)
|
|
|
|
|
.style(|_: &Theme| container::Style {
|
|
|
|
|
background: Some(Background::Color(Color {
|
|
|
|
|
r: 0.10,
|
|
|
|
|
g: 0.10,
|
|
|
|
|
b: 0.10,
|
|
|
|
|
a: 0.95,
|
|
|
|
|
})),
|
|
|
|
|
border: Border {
|
|
|
|
|
color: Color {
|
|
|
|
|
r: 0.40,
|
|
|
|
|
g: 0.40,
|
|
|
|
|
b: 0.40,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
},
|
|
|
|
|
width: 1.0,
|
|
|
|
|
radius: 3.0.into(),
|
|
|
|
|
},
|
|
|
|
|
..Default::default()
|
|
|
|
|
});
|
|
|
|
|
let anchor = iced::Point::new(popup.anchor.x + 12.0, popup.anchor.y + 12.0);
|
|
|
|
|
viewport_stack =
|
|
|
|
|
viewport_stack.push(position_canvas_overlay(anchor, panel.into()));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-12 02:07:26 +03:00
|
|
|
// Paper-space context actions: a right-edge vertical toolbar
|
|
|
|
|
// (viewport / page setup / plot) instead of a contextual ribbon tab.
|
|
|
|
|
if is_paper && !tab.is_start {
|
|
|
|
|
if let Some(tb) = crate::ui::side_toolbar::view(
|
|
|
|
|
&crate::modules::layout::paper_space_tools(),
|
|
|
|
|
) {
|
|
|
|
|
viewport_stack = viewport_stack.push(tb);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-21 11:46:43 +03:00
|
|
|
// In-place block edit (REFEDIT): right-edge toolbar with Save / Discard
|
|
|
|
|
// so the edit can be finished by clicking. (#136)
|
|
|
|
|
if tab.refedit_session.is_some() && !tab.is_start {
|
|
|
|
|
if let Some(tb) = crate::ui::side_toolbar::view(
|
|
|
|
|
&crate::modules::draw::modify::refedit::refedit_tools(),
|
|
|
|
|
) {
|
|
|
|
|
viewport_stack = viewport_stack.push(tb);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-04 23:31:36 +03:00
|
|
|
// Quick Properties: compact floating property panel on selection,
|
|
|
|
|
// anchored at the canvas top-left so it doesn't track the cursor.
|
|
|
|
|
if self.quick_properties && !tab.is_start {
|
|
|
|
|
if let Some(panel) = tab.properties.quick_view() {
|
|
|
|
|
viewport_stack = viewport_stack
|
|
|
|
|
.push(position_canvas_overlay(iced::Point::new(12.0, 12.0), panel));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-05 01:07:17 +03:00
|
|
|
// Frame-budget HUD (Phase 5.3): toggle with the PERF command. Shows
|
|
|
|
|
// the cost of the most recent wire re-tessellation — the work avoided
|
|
|
|
|
// by a warm wire cache — so render-path changes can be compared
|
|
|
|
|
// PR-to-PR. Reads ~0 ms while panning/zooming on a hit cache.
|
|
|
|
|
if self.perf_hud && !tab.is_start {
|
|
|
|
|
let s = &tab.scene;
|
|
|
|
|
let label = format!(
|
|
|
|
|
"tess {:.1} ms · {} wires · epoch {}",
|
|
|
|
|
s.last_tess_ms.get(),
|
|
|
|
|
s.last_tess_wires.get(),
|
|
|
|
|
s.geometry_epoch,
|
|
|
|
|
);
|
|
|
|
|
let panel = container(text(label).size(12).color(Color {
|
|
|
|
|
r: 0.6,
|
|
|
|
|
g: 1.0,
|
|
|
|
|
b: 0.6,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
}))
|
|
|
|
|
.padding(6)
|
|
|
|
|
.style(|_: &Theme| container::Style {
|
|
|
|
|
background: Some(Background::Color(Color {
|
|
|
|
|
r: 0.08,
|
|
|
|
|
g: 0.08,
|
|
|
|
|
b: 0.08,
|
|
|
|
|
a: 0.85,
|
|
|
|
|
})),
|
|
|
|
|
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()
|
|
|
|
|
});
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
viewport_stack = viewport_stack.push(position_canvas_overlay(
|
|
|
|
|
iced::Point::new(12.0, 40.0),
|
|
|
|
|
panel.into(),
|
|
|
|
|
));
|
2026-06-05 01:07:17 +03:00
|
|
|
}
|
|
|
|
|
|
2026-06-04 23:56:24 +03:00
|
|
|
// Selection-cycling list box: pick among overlapping objects.
|
|
|
|
|
if let Some((pt, cands)) = &self.cycle_candidates {
|
|
|
|
|
if !tab.is_start {
|
|
|
|
|
let items: Vec<(acadrust::Handle, String)> = cands
|
|
|
|
|
.iter()
|
|
|
|
|
.filter_map(|&h| {
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
tab.scene
|
|
|
|
|
.document
|
|
|
|
|
.get_entity(h)
|
|
|
|
|
.map(|e| (h, crate::entities::traits::entity_type_name(e).to_string()))
|
2026-06-04 23:56:24 +03:00
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
if !items.is_empty() {
|
|
|
|
|
viewport_stack = viewport_stack
|
|
|
|
|
.push(crate::ui::cycle_popup::cycle_popup_overlay(*pt, items));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-29 15:58:23 +03:00
|
|
|
// Right-click context menu. Lives inside the viewport stack so
|
|
|
|
|
// the cursor position (canvas-relative) anchors the menu under
|
|
|
|
|
// the cursor instead of drifting into window-relative space.
|
|
|
|
|
if !tab.is_start {
|
feat(draworder): cross-type draw order + context menu
DRAWORDER now affects rendering immediately (bump geometry epoch so the
sort cache rebuilds) and front/back anchor to the block's full effective
sort range instead of only existing table entries, so bring-to-front
actually lands on top. The render fallback sorts table-absent entities by
their own handle.
Draw order now applies across entity types, not just within a type. A
per-entity draw-order rank (SortEntitiesTable override or own handle,
ranked per block) is encoded as a small signed clip-z bias in the wire,
hatch, image, and 3DFACE pipelines. The shared LessEqual depth buffer then
orders fills, lines, rasters and 2D solids against each other regardless of
pass order — without touching model coordinates. 3D meshes keep real
geometric depth; wipeouts stay always-on-top.
Adds a Draw Order submenu to the viewport context menu (Bring to Front,
Send to Back, Bring Above/Send Under Object). The above/under items run an
interactive reference-object pick, then reorder the captured selection.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 21:29:01 +03:00
|
|
|
let (ctx_pos, draworder_open) = {
|
|
|
|
|
let sel = tab.scene.selection.borrow();
|
|
|
|
|
(sel.context_menu, sel.draworder_submenu)
|
|
|
|
|
};
|
2026-05-29 15:58:23 +03:00
|
|
|
if let Some(p) = ctx_pos {
|
|
|
|
|
let has_cmd = tab.active_cmd.is_some();
|
|
|
|
|
let has_selection = !tab.scene.selected.is_empty();
|
2026-06-04 23:17:21 +03:00
|
|
|
let isolation_active = tab.scene.is_isolation_active();
|
2026-05-29 15:58:23 +03:00
|
|
|
let last_cmds: Vec<String> = self
|
|
|
|
|
.command_line
|
|
|
|
|
.cmd_recall
|
|
|
|
|
.iter()
|
|
|
|
|
.rev()
|
|
|
|
|
.take(3)
|
|
|
|
|
.cloned()
|
|
|
|
|
.collect();
|
feat(draworder): cross-type draw order + context menu
DRAWORDER now affects rendering immediately (bump geometry epoch so the
sort cache rebuilds) and front/back anchor to the block's full effective
sort range instead of only existing table entries, so bring-to-front
actually lands on top. The render fallback sorts table-absent entities by
their own handle.
Draw order now applies across entity types, not just within a type. A
per-entity draw-order rank (SortEntitiesTable override or own handle,
ranked per block) is encoded as a small signed clip-z bias in the wire,
hatch, image, and 3DFACE pipelines. The shared LessEqual depth buffer then
orders fills, lines, rasters and 2D solids against each other regardless of
pass order — without touching model coordinates. 3D meshes keep real
geometric depth; wipeouts stay always-on-top.
Adds a Draw Order submenu to the viewport context menu (Bring to Front,
Send to Back, Bring Above/Send Under Object). The above/under items run an
interactive reference-object pick, then reorder the captured selection.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 21:29:01 +03:00
|
|
|
viewport_stack = viewport_stack.push(viewport_context_menu_overlay(
|
|
|
|
|
p,
|
|
|
|
|
has_cmd,
|
|
|
|
|
has_selection,
|
2026-06-04 23:17:21 +03:00
|
|
|
isolation_active,
|
feat(draworder): cross-type draw order + context menu
DRAWORDER now affects rendering immediately (bump geometry epoch so the
sort cache rebuilds) and front/back anchor to the block's full effective
sort range instead of only existing table entries, so bring-to-front
actually lands on top. The render fallback sorts table-absent entities by
their own handle.
Draw order now applies across entity types, not just within a type. A
per-entity draw-order rank (SortEntitiesTable override or own handle,
ranked per block) is encoded as a small signed clip-z bias in the wire,
hatch, image, and 3DFACE pipelines. The shared LessEqual depth buffer then
orders fills, lines, rasters and 2D solids against each other regardless of
pass order — without touching model coordinates. 3D meshes keep real
geometric depth; wipeouts stay always-on-top.
Adds a Draw Order submenu to the viewport context menu (Bring to Front,
Send to Back, Bring Above/Send Under Object). The above/under items run an
interactive reference-object pick, then reorder the captured selection.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 21:29:01 +03:00
|
|
|
last_cmds,
|
|
|
|
|
draworder_open,
|
|
|
|
|
));
|
feat(grip): multi-functional hover popup menu
Phase 2 of the grip overhaul: when the cursor dwells on a selected
entity's grip the overlay opens a popup menu with entity-specific
options (Stretch / Add Vertex / Remove Vertex / Convert to
Arc-Line / …). Picking an item dispatches `apply_grip_menu`; the
default `Stretch` entry is a no-op that lets the user click the
grip to start the normal drag.
- `GripMenuItem` + `GripMenuAction` (full vocabulary covering
stretch, add/remove vertex, convert to arc/line, reverse arrows,
text-position variants, hatch parameters, tangent direction).
- `Grippable` trait gains `grip_menu` + `apply_grip_menu` with
trait-default `Stretch`-only impls so every existing entity gets
the popup for free.
- `EntityTypeOps` dispatches both methods across the same entity
list as the existing grip / property machinery.
- `OpenCADStudio` carries `grip_hover` (dwell tracker) +
`grip_popup` (open menu). `update_grip_hover` runs from
`ViewportMove`: hit-tests grips, refreshes the dwell timer, and
opens the popup after 600 ms. Cursor drift past 80 px or a grip
click dismisses it.
- `view.rs` paints the popup as an absolute-positioned column of
buttons at the grip's screen anchor; each button publishes
`Message::GripMenuPick(idx)`.
- `LwPolyline` overrides `Grippable` manually (skips the standard
macro) so its vertex grip exposes Add/Remove Vertex and its
segment-midpoint grip exposes Add Vertex + Convert to Arc/Line.
`apply_grip_menu` performs the edits: vertex insertion at the
chord midpoint inheriting the previous bulge, vertex removal,
and bulge toggle for the arc/line conversion.
2026-05-29 09:30:54 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
// In-place MText editor (toolbar + text area), anchored at the
|
|
|
|
|
// insertion-point click.
|
|
|
|
|
if !tab.is_start {
|
2026-06-03 10:59:59 +03:00
|
|
|
let canvas = tab.scene.selection.borrow().vp_size;
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
if let Some(ed) = &self.mtext_editor {
|
|
|
|
|
let styles: Vec<String> = tab
|
|
|
|
|
.scene
|
|
|
|
|
.document
|
|
|
|
|
.text_styles
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|s| s.name.clone())
|
|
|
|
|
.collect();
|
2026-06-03 10:59:59 +03:00
|
|
|
viewport_stack = viewport_stack.push(mtext_editor_overlay(ed, styles, canvas));
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
}
|
2026-05-31 14:44:18 +03:00
|
|
|
if let Some(ed) = &self.text_inline {
|
2026-06-03 10:59:59 +03:00
|
|
|
viewport_stack = viewport_stack.push(text_inline_overlay(ed, canvas));
|
2026-05-31 14:44:18 +03:00
|
|
|
}
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
}
|
|
|
|
|
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
// Properties / layers panels carry no useful state on the Start tab.
|
|
|
|
|
// Replace the properties panel with a Recent Documents list there.
|
|
|
|
|
let properties_el: Element<'_, Message> = if tab.is_start {
|
|
|
|
|
recent_files_panel(&self.app_menu.recent)
|
2026-06-04 19:14:04 +03:00
|
|
|
} else if self.show_properties && !self.clean_screen {
|
2026-04-24 01:23:06 +03:00
|
|
|
tab.properties.view()
|
|
|
|
|
} else {
|
|
|
|
|
Space::new().into()
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-26 19:23:19 +03:00
|
|
|
// Command-line sits as a bottom-centre overlay on top of the
|
|
|
|
|
// viewport stack rather than as a separate row in the main
|
|
|
|
|
// column — frees up vertical space when no command is active
|
|
|
|
|
// and keeps the input close to where the cursor is drawing.
|
2026-05-26 22:45:54 +03:00
|
|
|
// Autocomplete shows only when no command is collecting its
|
|
|
|
|
// own input (otherwise typed prefixes are coordinates / values).
|
|
|
|
|
let allow_autocomplete = tab.active_cmd.is_none();
|
feat(dyn-input): TAB-editable distance/angle/coordinate fields
Dinamik giriş artık cursor yanında düzenlenebilir kutular gösteriyor.
TAB ile kutular arası geçilir, bir kutuya yazınca o kutu kilitlenir
(değer sabitlenir), diğerleri cursor'ı takip etmeye devam eder, Enter
ile çözülen nokta komuta point-pick olarak iletilir.
- DocumentTab.dyn_fields / dyn_active state; DynComponent + DynFieldEntry
- sync_dyn_fields komutun dyn_field() + base noktasına göre kutu kümesini
türetir: nokta+base → [d, açı], nokta+base yok → [X, Y], radius → [d],
açı → [açı]
- klavye yönlendirme: rakam/./-/+ aktif kutuya, TAB sonraki kutuya,
Enter try_dyn_commit, Backspace kilidi açar
- dyn_capturing iken command-line text alanı on_input'suz bırakılıyor
ki focus'u ve tuşları çalmasın; girişler global key aboneliğinden
geliyor (numpad dahil text alanı üzerinden yakalanıyor)
- çoklu-kutu overlay render: aktif kutu vurgulu, kilitli kutu farklı
kenar rengi
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:08:57 +03:00
|
|
|
// Dynamic input captures keystrokes when its fields are showing,
|
|
|
|
|
// so the command-line field must release focus / its on_input.
|
2026-05-31 10:16:30 +03:00
|
|
|
// The MText preview also captures keystrokes (typing edits it), so the
|
|
|
|
|
// command line must likewise release its on_input there.
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
let dyn_capturing =
|
|
|
|
|
(self.dyn_input && tab.active_cmd.is_some() && !tab.dyn_fields.is_empty())
|
|
|
|
|
|| self.mtext_editor.as_ref().is_some_and(|e| e.show_preview)
|
|
|
|
|
|| self.text_inline.is_some();
|
feat(dyn-input): TAB-editable distance/angle/coordinate fields
Dinamik giriş artık cursor yanında düzenlenebilir kutular gösteriyor.
TAB ile kutular arası geçilir, bir kutuya yazınca o kutu kilitlenir
(değer sabitlenir), diğerleri cursor'ı takip etmeye devam eder, Enter
ile çözülen nokta komuta point-pick olarak iletilir.
- DocumentTab.dyn_fields / dyn_active state; DynComponent + DynFieldEntry
- sync_dyn_fields komutun dyn_field() + base noktasına göre kutu kümesini
türetir: nokta+base → [d, açı], nokta+base yok → [X, Y], radius → [d],
açı → [açı]
- klavye yönlendirme: rakam/./-/+ aktif kutuya, TAB sonraki kutuya,
Enter try_dyn_commit, Backspace kilidi açar
- dyn_capturing iken command-line text alanı on_input'suz bırakılıyor
ki focus'u ve tuşları çalmasın; girişler global key aboneliğinden
geliyor (numpad dahil text alanı üzerinden yakalanıyor)
- çoklu-kutu overlay render: aktif kutu vurgulu, kilitli kutu farklı
kenar rengi
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:08:57 +03:00
|
|
|
let command_line_overlay =
|
2026-06-09 18:46:45 +03:00
|
|
|
iced::widget::container(self.command_line.view(allow_autocomplete, dyn_capturing))
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
.width(Fill)
|
|
|
|
|
.height(Fill)
|
|
|
|
|
.align_x(iced::alignment::Horizontal::Center)
|
|
|
|
|
.align_y(iced::alignment::Vertical::Bottom)
|
|
|
|
|
.padding(iced::Padding {
|
|
|
|
|
top: 0.0,
|
|
|
|
|
right: 0.0,
|
|
|
|
|
bottom: 2.0,
|
|
|
|
|
left: 0.0,
|
|
|
|
|
});
|
2026-05-26 19:23:19 +03:00
|
|
|
|
|
|
|
|
let center_stack = iced::widget::stack![
|
|
|
|
|
row![properties_el, viewport_stack].width(Fill).height(Fill),
|
|
|
|
|
command_line_overlay,
|
|
|
|
|
]
|
|
|
|
|
.width(Fill)
|
|
|
|
|
.height(Fill);
|
2026-03-22 22:44:32 +03:00
|
|
|
|
2026-04-24 01:23:06 +03:00
|
|
|
let main_ui = container({
|
2026-06-04 19:14:04 +03:00
|
|
|
// Clean-screen mode drops the ribbon for a full-canvas view; the
|
|
|
|
|
// status bar stays so the mode can be toggled back off.
|
|
|
|
|
let mut col = column![];
|
|
|
|
|
if !self.clean_screen {
|
|
|
|
|
col = col.push(self.ribbon.view(
|
|
|
|
|
is_paper,
|
|
|
|
|
self.tabs[self.active_tab].history.undo_stack.len(),
|
|
|
|
|
self.tabs[self.active_tab].history.redo_stack.len(),
|
|
|
|
|
));
|
|
|
|
|
}
|
2026-04-24 01:23:06 +03:00
|
|
|
if self.show_file_tabs {
|
|
|
|
|
col = col.push(doc_tab_bar(&self.tabs, self.active_tab));
|
|
|
|
|
}
|
|
|
|
|
col.push(center_stack)
|
2026-05-12 10:36:15 +03:00
|
|
|
.push({
|
2026-05-06 17:50:46 +03:00
|
|
|
let is_model = tab.scene.current_layout == "Model";
|
|
|
|
|
let scale_pill_enabled = is_model
|
|
|
|
|
|| tab.scene.active_viewport.is_some()
|
|
|
|
|
|| tab.scene.has_selected_viewport();
|
2026-06-04 19:14:04 +03:00
|
|
|
// The cursor is tracked in local render space; re-add the
|
|
|
|
|
// model-space world offset so the readout shows true
|
feat(ucs): central WCS↔UCS converter; adopt file UCS, drive readout + icon
Introduce one bridge between WCS (how geometry and the file are stored)
and the active UCS (the coordinate system the user works in), so every
UCS-aware system routes through it instead of re-deriving axis math.
- UcsXform (app/helpers.rs): the converter — to_wcs/to_ucs (points),
vec_to_wcs/vec_to_ucs (directions), axes(), is_identity(). Orthonormal
axes, so the inverse is the transpose. The old ucs_to_wcs / ucs_rotate_vec
free fns now delegate to it (one implementation).
- DocumentTab::ucs_xform() builds it from the tab's active_ucs (None = WCS).
- DocumentTab::adopt_active_ucs_from_header() loads the document's saved
current UCS (header model-space UCS) into active_ucs on open, so the file's
coordinate system is live immediately; called from both load paths.
- Status-bar coordinate readout reports the cursor in the active UCS.
- UCS icon tripod projects the active UCS axes, so it rotates to the UCS
instead of always showing world X/Y/Z.
First slice of routing the app's coordinate systems through the converter;
input/picking, grid, ViewCube, and the remaining readouts follow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 09:57:39 +03:00
|
|
|
// drawing coordinates (paper space carries no offset), then
|
|
|
|
|
// report it in the active UCS — the readout follows the
|
|
|
|
|
// user's coordinate system, not raw WCS (no-op without UCS).
|
2026-06-04 19:14:04 +03:00
|
|
|
let cursor_coord = {
|
|
|
|
|
let lc = tab.last_cursor_world;
|
|
|
|
|
if is_model {
|
2026-06-23 22:42:33 +03:00
|
|
|
let wo = [0.0_f64; 3];
|
feat(ucs): central WCS↔UCS converter; adopt file UCS, drive readout + icon
Introduce one bridge between WCS (how geometry and the file are stored)
and the active UCS (the coordinate system the user works in), so every
UCS-aware system routes through it instead of re-deriving axis math.
- UcsXform (app/helpers.rs): the converter — to_wcs/to_ucs (points),
vec_to_wcs/vec_to_ucs (directions), axes(), is_identity(). Orthonormal
axes, so the inverse is the transpose. The old ucs_to_wcs / ucs_rotate_vec
free fns now delegate to it (one implementation).
- DocumentTab::ucs_xform() builds it from the tab's active_ucs (None = WCS).
- DocumentTab::adopt_active_ucs_from_header() loads the document's saved
current UCS (header model-space UCS) into active_ucs on open, so the file's
coordinate system is live immediately; called from both load paths.
- Status-bar coordinate readout reports the cursor in the active UCS.
- UCS icon tripod projects the active UCS axes, so it rotates to the UCS
instead of always showing world X/Y/Z.
First slice of routing the app's coordinate systems through the converter;
input/picking, grid, ViewCube, and the remaining readouts follow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 09:57:39 +03:00
|
|
|
let wcs = glam::Vec3::new(
|
2026-06-04 19:14:04 +03:00
|
|
|
lc.x + wo[0] as f32,
|
|
|
|
|
lc.y + wo[1] as f32,
|
|
|
|
|
lc.z + wo[2] as f32,
|
feat(ucs): central WCS↔UCS converter; adopt file UCS, drive readout + icon
Introduce one bridge between WCS (how geometry and the file are stored)
and the active UCS (the coordinate system the user works in), so every
UCS-aware system routes through it instead of re-deriving axis math.
- UcsXform (app/helpers.rs): the converter — to_wcs/to_ucs (points),
vec_to_wcs/vec_to_ucs (directions), axes(), is_identity(). Orthonormal
axes, so the inverse is the transpose. The old ucs_to_wcs / ucs_rotate_vec
free fns now delegate to it (one implementation).
- DocumentTab::ucs_xform() builds it from the tab's active_ucs (None = WCS).
- DocumentTab::adopt_active_ucs_from_header() loads the document's saved
current UCS (header model-space UCS) into active_ucs on open, so the file's
coordinate system is live immediately; called from both load paths.
- Status-bar coordinate readout reports the cursor in the active UCS.
- UCS icon tripod projects the active UCS axes, so it rotates to the UCS
instead of always showing world X/Y/Z.
First slice of routing the app's coordinate systems through the converter;
input/picking, grid, ViewCube, and the remaining readouts follow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 09:57:39 +03:00
|
|
|
);
|
|
|
|
|
tab.ucs_xform().to_ucs(wcs)
|
2026-06-04 19:14:04 +03:00
|
|
|
} else {
|
|
|
|
|
lc
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-05-06 17:50:46 +03:00
|
|
|
self.status_bar.view(
|
|
|
|
|
&self.snapper,
|
|
|
|
|
self.snap_popup_open,
|
|
|
|
|
self.ortho_mode,
|
|
|
|
|
self.polar_mode,
|
|
|
|
|
self.polar_increment_deg,
|
|
|
|
|
self.dyn_input,
|
|
|
|
|
self.snapper.otrack_enabled,
|
|
|
|
|
tab.scene.layout_names(),
|
|
|
|
|
tab.scene.current_layout.clone(),
|
|
|
|
|
self.layout_rename_state.as_ref(),
|
|
|
|
|
tab.scene.first_viewport_scale(),
|
|
|
|
|
tab.scene.viewport_count(),
|
|
|
|
|
tab.scene.active_viewport.is_some(),
|
|
|
|
|
self.show_layout_tabs,
|
|
|
|
|
tab.scene.annotation_scale,
|
|
|
|
|
self.scale_popup_open,
|
|
|
|
|
scale_pill_enabled,
|
2026-05-23 07:15:26 +03:00
|
|
|
tab.scene.document.header.lineweight_display,
|
2026-06-04 19:14:04 +03:00
|
|
|
cursor_coord,
|
|
|
|
|
self.clean_screen,
|
2026-06-04 22:06:42 +03:00
|
|
|
tab.scene.document.header.insertion_units,
|
|
|
|
|
self.units_popup_open,
|
2026-06-04 22:51:23 +03:00
|
|
|
tab.scene.is_isolation_active(),
|
2026-06-04 23:25:09 +03:00
|
|
|
tab.scene.transparency_display,
|
2026-06-04 23:31:36 +03:00
|
|
|
self.quick_properties,
|
2026-06-04 23:39:48 +03:00
|
|
|
tab.scene.selection_filter_active(),
|
2026-06-04 23:44:39 +03:00
|
|
|
self.selection_cycling,
|
2026-06-04 19:14:04 +03:00
|
|
|
&self.statusbar_config,
|
2026-05-06 17:50:46 +03:00
|
|
|
)
|
2026-05-12 10:36:15 +03:00
|
|
|
})
|
|
|
|
|
.width(Fill)
|
|
|
|
|
.height(Fill)
|
2026-04-24 01:23:06 +03:00
|
|
|
})
|
2026-03-22 22:44:32 +03:00
|
|
|
.style(|_: &Theme| container::Style {
|
2026-05-12 10:36:15 +03:00
|
|
|
background: Some(Background::Color(Color {
|
|
|
|
|
r: 0.11,
|
|
|
|
|
g: 0.11,
|
|
|
|
|
b: 0.11,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
})),
|
2026-03-22 22:44:32 +03:00
|
|
|
..Default::default()
|
|
|
|
|
})
|
|
|
|
|
.width(Fill)
|
|
|
|
|
.height(Fill);
|
|
|
|
|
|
|
|
|
|
let snap_layer: Element<'_, Message> = if self.snap_popup_open {
|
|
|
|
|
crate::ui::snap_popup::snap_popup_overlay(&self.snapper, 4.0)
|
|
|
|
|
} else {
|
|
|
|
|
iced::widget::Space::new().width(0).height(0).into()
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-06 17:50:46 +03:00
|
|
|
let scale_layer: Element<'_, Message> = if self.scale_popup_open {
|
|
|
|
|
let is_model = tab.scene.current_layout == "Model";
|
|
|
|
|
crate::ui::scale_popup::scale_popup_overlay(
|
|
|
|
|
is_model,
|
|
|
|
|
tab.scene.annotation_scale,
|
|
|
|
|
tab.scene.first_viewport_scale(),
|
2026-06-04 16:18:01 +03:00
|
|
|
tab.scene.scale_list(),
|
2026-05-06 17:50:46 +03:00
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
iced::widget::Space::new().width(0).height(0).into()
|
|
|
|
|
};
|
|
|
|
|
|
2026-06-04 19:14:04 +03:00
|
|
|
let statusbar_menu_layer: Element<'_, Message> = if self.statusbar_menu_open {
|
|
|
|
|
crate::ui::statusbar_menu::statusbar_menu_overlay(&self.statusbar_config)
|
|
|
|
|
} else {
|
|
|
|
|
iced::widget::Space::new().width(0).height(0).into()
|
|
|
|
|
};
|
|
|
|
|
|
2026-06-04 22:06:42 +03:00
|
|
|
let units_layer: Element<'_, Message> = if self.units_popup_open {
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
crate::ui::units_popup::units_popup_overlay(tab.scene.document.header.insertion_units)
|
2026-06-04 22:06:42 +03:00
|
|
|
} else {
|
|
|
|
|
iced::widget::Space::new().width(0).height(0).into()
|
|
|
|
|
};
|
|
|
|
|
|
2026-06-04 23:17:21 +03:00
|
|
|
let isolate_layer: Element<'_, Message> = if self.isolate_popup_open {
|
|
|
|
|
crate::ui::isolate_popup::isolate_popup_overlay(
|
|
|
|
|
!tab.scene.selected.is_empty(),
|
|
|
|
|
tab.scene.is_isolation_active(),
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
iced::widget::Space::new().width(0).height(0).into()
|
|
|
|
|
};
|
|
|
|
|
|
2026-06-04 23:39:48 +03:00
|
|
|
let sel_filter_layer: Element<'_, Message> = if self.selection_filter_popup_open {
|
|
|
|
|
let types: Vec<String> = tab
|
|
|
|
|
.scene
|
|
|
|
|
.entity_type_names_in_layout()
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|s| s.to_string())
|
|
|
|
|
.collect();
|
|
|
|
|
crate::ui::selection_filter_popup::selection_filter_popup_overlay(
|
|
|
|
|
types,
|
|
|
|
|
&tab.scene.selection_filter,
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
iced::widget::Space::new().width(0).height(0).into()
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-22 22:44:32 +03:00
|
|
|
let dropdown_layer: Element<'_, Message> = self
|
|
|
|
|
.ribbon
|
|
|
|
|
.dropdown_overlay(
|
|
|
|
|
&history_dropdown_labels(&self.tabs[self.active_tab].history.undo_stack),
|
|
|
|
|
&history_dropdown_labels(&self.tabs[self.active_tab].history.redo_stack),
|
|
|
|
|
)
|
|
|
|
|
.unwrap_or_else(|| iced::widget::Space::new().width(0).height(0).into());
|
|
|
|
|
|
2026-05-12 10:36:15 +03:00
|
|
|
let layout_ctx_layer: Element<'_, Message> = if let Some(name) = &self.layout_context_menu {
|
|
|
|
|
layout_context_menu_overlay(name)
|
|
|
|
|
} else {
|
|
|
|
|
iced::widget::Space::new().width(0).height(0).into()
|
|
|
|
|
};
|
2026-03-31 07:13:36 +03:00
|
|
|
|
2026-05-29 20:30:56 +03:00
|
|
|
let qselect_layer: Element<'_, Message> = if let Some(state) = &self.qselect {
|
|
|
|
|
let types = tab.scene.entity_type_names_in_layout();
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
let properties = tab.scene.qselect_properties(state.type_filter.as_deref());
|
feat(qselect): per-property filter with operator + value (#28)
Quick Select used to filter only on Object Type and Layer. The classic
QSELECT model — pick a type, pick one of its properties, compare with a
typed value via a logical operator — is much more general: a Line entity
can now be filtered on Start X, Length, Angle, Delta Y, …, and any
entity on the common Layer / Color / Linetype / Lineweight.
Panel layout: Object type → Property → Operator → Value → Append. The
Property dropdown is type-aware: it always lists the four common
properties; picking a specific Object type appends that type's
`geometry_properties` rows, sampled from the first entity of that type
in the active layout (so `Start X`, `Radius`, `Pattern Angle`, … only
appear when their entity type is selected). Changing the Object type
drops the Property when the new type no longer offers it.
Operators: `= Equals`, `!= Not equal`, `> Greater than`, `< Less than`,
`* Any value`. Eq/Neq do case-insensitive string compare against the
canonical property string (so "ByLayer" matches "bylayer"); Gt/Lt parse
both sides as `f64` and reject anything non-numeric; Any skips the
value test entirely. The Value text field is disabled when no Property
is picked or the operator is Any. Append behaves the same as before.
The new value reader is hand-rolled for the four common properties
(layer name, color formatted as `ByLayer`/`ByBlock`/`<aci>`/`r,g,b`,
linetype name, lineweight formatted as `0.30mm`) and falls back through
`geometry_properties()` for type-specific fields — extracting the
property's value out of the `PropValue` variant so EditText / Choice /
ColorChoice all canonicalise the same way the panel will display them.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 21:09:56 +03:00
|
|
|
qselect_overlay(state, &types, &properties)
|
2026-05-29 20:30:56 +03:00
|
|
|
} else {
|
|
|
|
|
iced::widget::Space::new().width(0).height(0).into()
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-13 09:59:07 +03:00
|
|
|
let open_progress_layer: Element<'_, Message> = if let Some(p) = &self.opening {
|
|
|
|
|
crate::ui::open_progress::view(p, iced::time::Instant::now())
|
|
|
|
|
} else {
|
|
|
|
|
iced::widget::Space::new().width(0).height(0).into()
|
|
|
|
|
};
|
|
|
|
|
|
2026-06-16 18:59:13 +03:00
|
|
|
let composed = stack![
|
2026-05-12 10:36:15 +03:00
|
|
|
main_ui,
|
|
|
|
|
self.app_menu.view(),
|
|
|
|
|
snap_layer,
|
|
|
|
|
scale_layer,
|
2026-06-04 19:14:04 +03:00
|
|
|
statusbar_menu_layer,
|
2026-06-04 22:06:42 +03:00
|
|
|
units_layer,
|
2026-06-04 23:17:21 +03:00
|
|
|
isolate_layer,
|
2026-06-04 23:39:48 +03:00
|
|
|
sel_filter_layer,
|
2026-05-12 10:36:15 +03:00
|
|
|
dropdown_layer,
|
|
|
|
|
layout_ctx_layer,
|
2026-05-29 20:30:56 +03:00
|
|
|
qselect_layer,
|
2026-05-13 09:59:07 +03:00
|
|
|
open_progress_layer,
|
2026-06-16 18:59:13 +03:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// ── In-canvas modal dialogs (Plan B) ───────────────────────────────
|
|
|
|
|
// Former pop-up windows render as overlays here, so they work on both
|
|
|
|
|
// the native (single main window) and web builds.
|
2026-06-16 23:42:14 +03:00
|
|
|
let base: Element<'_, Message> = match self.modal_content() {
|
2026-06-17 09:59:17 +03:00
|
|
|
Some(content) => {
|
|
|
|
|
crate::ui::modal::modal(composed, content, Message::CloseModal, self.modal_offset)
|
|
|
|
|
}
|
2026-06-16 23:22:54 +03:00
|
|
|
None => composed.into(),
|
2026-06-16 23:42:14 +03:00
|
|
|
};
|
|
|
|
|
// The colour picker is a nested modal: it stacks over whichever dialog
|
|
|
|
|
// (style editor, properties, …) requested it.
|
|
|
|
|
if self.color_pick_target.is_some() {
|
|
|
|
|
crate::ui::modal::modal(
|
|
|
|
|
base,
|
|
|
|
|
iced::widget::container(crate::ui::color_select::color_grid_window(
|
|
|
|
|
Message::ColorWindowPick,
|
|
|
|
|
))
|
|
|
|
|
.width(iced::Length::Fixed(420.0))
|
|
|
|
|
.height(iced::Length::Fixed(470.0)),
|
|
|
|
|
Message::CloseColorPicker,
|
2026-06-17 09:59:17 +03:00
|
|
|
iced::Vector::ZERO,
|
2026-06-16 23:42:14 +03:00
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
base
|
2026-06-16 23:22:54 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-17 09:59:17 +03:00
|
|
|
/// Outer pixel size (content + title-bar/padding chrome) of the active
|
|
|
|
|
/// modal, used to clamp drag so it cannot be pushed off-screen. Mirrors the
|
|
|
|
|
/// `sized(..)` dimensions in [`Self::modal_content`]; keep the two in sync.
|
|
|
|
|
/// `None` has no active modal. About (content-sized) uses a safe estimate.
|
|
|
|
|
pub(crate) fn modal_outer_size(&self) -> Option<(f32, f32)> {
|
|
|
|
|
use super::ModalKind::*;
|
|
|
|
|
// Title bar (~26) + spacing (6) + frame padding (10·2) → ~52 vertical;
|
|
|
|
|
// frame padding → ~20 horizontal.
|
|
|
|
|
const EXTRA_W: f32 = 20.0;
|
|
|
|
|
const EXTRA_H: f32 = 52.0;
|
|
|
|
|
let (w, h) = match self.active_modal? {
|
|
|
|
|
About => (440, 360),
|
|
|
|
|
Shortcuts => (720, 520),
|
|
|
|
|
PluginManager => (520, 460),
|
|
|
|
|
UpdateNotice => (560, 460),
|
|
|
|
|
Layers => (900, 360),
|
|
|
|
|
PageSetup => (520, 460),
|
|
|
|
|
LayoutManager => (640, 320),
|
|
|
|
|
Plotstyle => (780, 540),
|
|
|
|
|
TextStyle => (860, 480),
|
|
|
|
|
MlStyle => (620, 420),
|
|
|
|
|
TableStyle => (620, 420),
|
|
|
|
|
MLeaderStyle => (560, 560),
|
|
|
|
|
DimStyle => (720, 560),
|
|
|
|
|
AssocPrompt => (440, 210),
|
|
|
|
|
Unsaved => (420, 160),
|
|
|
|
|
SaveDialog => (560, 480),
|
2026-06-17 11:13:12 +03:00
|
|
|
PointStyle => (360, 470),
|
2026-06-17 09:59:17 +03:00
|
|
|
};
|
|
|
|
|
Some((w as f32 + EXTRA_W, h as f32 + EXTRA_H))
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-16 23:22:54 +03:00
|
|
|
/// Build the currently-open modal dialog's content (Plan B), or `None`.
|
|
|
|
|
/// Each former pop-up window is constructed here and given a bounded size
|
|
|
|
|
/// (About shrinks to its content). Rendered as an overlay by `view_main`.
|
|
|
|
|
fn modal_content(&self) -> Option<Element<'_, Message>> {
|
|
|
|
|
fn sized<'a>(e: Element<'a, Message>, w: u16, h: u16) -> Element<'a, Message> {
|
|
|
|
|
iced::widget::container(e)
|
|
|
|
|
.width(iced::Length::Fixed(w as f32))
|
|
|
|
|
.height(iced::Length::Fixed(h as f32))
|
|
|
|
|
.into()
|
|
|
|
|
}
|
|
|
|
|
Some(match self.active_modal? {
|
|
|
|
|
super::ModalKind::About => crate::ui::about::view_window(),
|
|
|
|
|
super::ModalKind::Shortcuts => {
|
|
|
|
|
sized(crate::ui::shortcuts::view_window(&self.shortcut_overrides), 720, 520)
|
2026-06-16 19:07:25 +03:00
|
|
|
}
|
2026-06-16 23:22:54 +03:00
|
|
|
super::ModalKind::PluginManager => sized(
|
2026-06-17 10:15:38 +03:00
|
|
|
crate::ui::plugin_manager::view_window(
|
|
|
|
|
&self.disabled_plugins,
|
2026-06-17 11:43:24 +03:00
|
|
|
&self.external_plugins,
|
2026-06-17 14:48:56 +03:00
|
|
|
&self.loaded_plugin_ids,
|
2026-06-17 15:24:56 +03:00
|
|
|
crate::ui::plugin_manager::MarketView {
|
|
|
|
|
registry: &self.plugin_registry,
|
|
|
|
|
input: &self.plugin_repo_input,
|
|
|
|
|
repos: &self.plugin_repos,
|
|
|
|
|
release_tags: &self.repo_release_tags,
|
|
|
|
|
selected_tag: &self.repo_selected_tag,
|
|
|
|
|
status: &self.marketplace_status,
|
|
|
|
|
},
|
2026-06-17 10:15:38 +03:00
|
|
|
),
|
2026-06-16 23:22:54 +03:00
|
|
|
520,
|
|
|
|
|
460,
|
2026-06-16 19:07:25 +03:00
|
|
|
),
|
2026-06-16 23:22:54 +03:00
|
|
|
super::ModalKind::UpdateNotice => {
|
2026-06-16 19:07:25 +03:00
|
|
|
let latest = self.update_notice_version.as_deref().unwrap_or("?");
|
|
|
|
|
let body = self.update_notice_body.as_deref().unwrap_or("");
|
2026-06-16 23:22:54 +03:00
|
|
|
sized(crate::ui::update_notice::view_window(latest, body), 560, 460)
|
|
|
|
|
}
|
|
|
|
|
super::ModalKind::Layers => {
|
|
|
|
|
let tab = &self.tabs[self.active_tab];
|
|
|
|
|
sized(tab.layers.view_window(), 900, 360)
|
|
|
|
|
}
|
|
|
|
|
super::ModalKind::PageSetup => sized(
|
|
|
|
|
crate::ui::page_setup::view_window(
|
|
|
|
|
&self.page_setup_w,
|
|
|
|
|
&self.page_setup_h,
|
|
|
|
|
&self.page_setup_plot_area,
|
|
|
|
|
self.page_setup_center,
|
|
|
|
|
&self.page_setup_offset_x,
|
|
|
|
|
&self.page_setup_offset_y,
|
|
|
|
|
&self.page_setup_rotation,
|
|
|
|
|
&self.page_setup_scale,
|
|
|
|
|
),
|
|
|
|
|
520,
|
|
|
|
|
460,
|
|
|
|
|
),
|
|
|
|
|
super::ModalKind::LayoutManager => {
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
let layouts = self.tabs[i].scene.layout_names();
|
|
|
|
|
let current = self.tabs[i].scene.current_layout.clone();
|
|
|
|
|
sized(
|
|
|
|
|
crate::ui::layout_manager::view_window(
|
|
|
|
|
layouts,
|
|
|
|
|
&self.layout_manager_selected,
|
|
|
|
|
&self.layout_manager_rename_buf,
|
|
|
|
|
current,
|
|
|
|
|
),
|
|
|
|
|
640,
|
|
|
|
|
320,
|
2026-06-16 19:07:25 +03:00
|
|
|
)
|
|
|
|
|
}
|
2026-06-16 23:22:54 +03:00
|
|
|
super::ModalKind::Plotstyle => sized(
|
|
|
|
|
crate::ui::plotstyle::view_window(
|
|
|
|
|
self.active_plot_style.as_ref(),
|
|
|
|
|
self.plotstyle_panel_aci,
|
|
|
|
|
&self.ps_color_buf,
|
|
|
|
|
&self.ps_lineweight_buf,
|
|
|
|
|
&self.ps_screening_buf,
|
|
|
|
|
),
|
|
|
|
|
780,
|
|
|
|
|
540,
|
|
|
|
|
),
|
2026-06-16 23:29:50 +03:00
|
|
|
super::ModalKind::TextStyle => {
|
|
|
|
|
let tab = &self.tabs[self.active_tab];
|
|
|
|
|
let styles: Vec<String> = tab
|
|
|
|
|
.scene
|
|
|
|
|
.document
|
|
|
|
|
.text_styles
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|s| s.name.clone())
|
|
|
|
|
.collect();
|
|
|
|
|
let (backward, upside_down, annotative) = tab
|
|
|
|
|
.scene
|
|
|
|
|
.document
|
|
|
|
|
.text_styles
|
|
|
|
|
.get(&self.textstyle_selected)
|
|
|
|
|
.map(|s| (s.flags.backward, s.flags.upside_down, s.annotative))
|
|
|
|
|
.unwrap_or((false, false, false));
|
|
|
|
|
sized(
|
|
|
|
|
crate::ui::textstyle::view_window(crate::ui::textstyle::TextStyleView {
|
|
|
|
|
styles,
|
|
|
|
|
selected: &self.textstyle_selected,
|
|
|
|
|
current: &tab.scene.document.header.current_text_style_name,
|
|
|
|
|
font_buf: &self.textstyle_font,
|
|
|
|
|
width_buf: &self.textstyle_width,
|
|
|
|
|
oblique_buf: &self.textstyle_oblique,
|
|
|
|
|
height_buf: &self.textstyle_height,
|
|
|
|
|
bigfont_buf: &self.textstyle_bigfont,
|
|
|
|
|
ttf_buf: &self.textstyle_ttf,
|
|
|
|
|
backward,
|
|
|
|
|
upside_down,
|
|
|
|
|
annotative,
|
|
|
|
|
rename_active: self.style_rename.as_deref(),
|
|
|
|
|
rename_buf: &self.style_rename_buf,
|
|
|
|
|
}),
|
2026-06-16 23:45:40 +03:00
|
|
|
// Wider than the old 620 window: the TTF system-font panel
|
|
|
|
|
// (Plan B / web fonts) added a column.
|
|
|
|
|
860,
|
|
|
|
|
480,
|
2026-06-16 23:29:50 +03:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
super::ModalKind::MlStyle => {
|
|
|
|
|
use acadrust::objects::ObjectType;
|
|
|
|
|
let tab = &self.tabs[self.active_tab];
|
|
|
|
|
let styles: Vec<String> = tab
|
|
|
|
|
.scene
|
|
|
|
|
.document
|
|
|
|
|
.objects
|
|
|
|
|
.values()
|
|
|
|
|
.filter_map(|o| match o {
|
|
|
|
|
ObjectType::MLineStyle(s) => Some(s.name.clone()),
|
|
|
|
|
_ => None,
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
let selected_style = tab.scene.document.objects.values().find_map(|o| match o {
|
|
|
|
|
ObjectType::MLineStyle(s) if s.name == self.mlstyle_selected => Some(s),
|
|
|
|
|
_ => None,
|
|
|
|
|
});
|
|
|
|
|
sized(
|
|
|
|
|
crate::ui::mlstyle::view_window(
|
|
|
|
|
styles,
|
|
|
|
|
&self.mlstyle_selected,
|
|
|
|
|
selected_style,
|
|
|
|
|
tab.scene.document.header.multiline_style.clone(),
|
|
|
|
|
self.style_rename.as_deref(),
|
|
|
|
|
&self.style_rename_buf,
|
|
|
|
|
),
|
|
|
|
|
620,
|
|
|
|
|
420,
|
|
|
|
|
)
|
|
|
|
|
}
|
2026-06-16 23:42:14 +03:00
|
|
|
super::ModalKind::TableStyle => {
|
|
|
|
|
use acadrust::objects::ObjectType;
|
|
|
|
|
let tab = &self.tabs[self.active_tab];
|
|
|
|
|
let styles: Vec<String> = tab
|
|
|
|
|
.scene
|
|
|
|
|
.document
|
|
|
|
|
.objects
|
|
|
|
|
.values()
|
|
|
|
|
.filter_map(|o| match o {
|
|
|
|
|
ObjectType::TableStyle(s) => Some(s.name.clone()),
|
|
|
|
|
_ => None,
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
let selected_style = tab.scene.document.objects.values().find_map(|o| match o {
|
|
|
|
|
ObjectType::TableStyle(s) if s.name == self.tablestyle_selected => Some(s),
|
|
|
|
|
_ => None,
|
|
|
|
|
});
|
|
|
|
|
sized(
|
|
|
|
|
crate::ui::tablestyle::view_window(
|
|
|
|
|
styles,
|
|
|
|
|
&self.tablestyle_selected,
|
|
|
|
|
&self.ribbon.active_table_style,
|
|
|
|
|
selected_style,
|
|
|
|
|
&self.ts_hmargin,
|
|
|
|
|
&self.ts_vmargin,
|
|
|
|
|
&self.ts_description,
|
|
|
|
|
&self.ts_cell_textstyle,
|
|
|
|
|
&self.ts_cell_height,
|
|
|
|
|
&self.ts_cell_textcolor,
|
|
|
|
|
&self.ts_cell_fillcolor,
|
|
|
|
|
&self.ts_cell_datatype,
|
|
|
|
|
&self.ts_cell_unittype,
|
|
|
|
|
&self.ts_cell_format,
|
|
|
|
|
&self.ts_border_lw,
|
|
|
|
|
&self.ts_border_color,
|
|
|
|
|
&self.ts_border_spacing,
|
|
|
|
|
self.style_rename.as_deref(),
|
|
|
|
|
&self.style_rename_buf,
|
|
|
|
|
self.ts_color_open,
|
|
|
|
|
),
|
|
|
|
|
620,
|
|
|
|
|
420,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
super::ModalKind::MLeaderStyle => {
|
|
|
|
|
use acadrust::objects::ObjectType;
|
|
|
|
|
let tab = &self.tabs[self.active_tab];
|
|
|
|
|
let styles: Vec<String> = tab
|
|
|
|
|
.scene
|
|
|
|
|
.document
|
|
|
|
|
.objects
|
|
|
|
|
.values()
|
|
|
|
|
.filter_map(|o| match o {
|
|
|
|
|
ObjectType::MultiLeaderStyle(s) => Some(s.name.clone()),
|
|
|
|
|
_ => None,
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
let selected_style = tab.scene.document.objects.values().find_map(|o| match o {
|
|
|
|
|
ObjectType::MultiLeaderStyle(s) if s.name == self.mleaderstyle_selected => {
|
|
|
|
|
Some(s)
|
|
|
|
|
}
|
|
|
|
|
_ => None,
|
|
|
|
|
});
|
|
|
|
|
let doc = &tab.scene.document;
|
|
|
|
|
let mut block_opts: Vec<String> = vec!["None".to_string()];
|
|
|
|
|
block_opts.extend(doc.block_records.iter().map(|b| b.name.clone()));
|
|
|
|
|
let mut lt_opts: Vec<String> = vec!["None".to_string()];
|
|
|
|
|
lt_opts.extend(doc.line_types.iter().map(|lt| lt.name.clone()));
|
|
|
|
|
let mut textstyle_opts: Vec<String> = vec!["None".to_string()];
|
|
|
|
|
textstyle_opts.extend(doc.text_styles.iter().map(|t| t.name.clone()));
|
|
|
|
|
let opt_block = |h: Option<acadrust::types::Handle>| -> String {
|
|
|
|
|
match h {
|
|
|
|
|
Some(h) => doc
|
|
|
|
|
.block_records
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|b| b.handle == h)
|
|
|
|
|
.map(|b| b.name.clone())
|
|
|
|
|
.unwrap_or_else(|| "None".to_string()),
|
|
|
|
|
None => "None".to_string(),
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
let opt_lt = |h: Option<acadrust::types::Handle>| -> String {
|
|
|
|
|
match h {
|
|
|
|
|
Some(h) => doc
|
|
|
|
|
.line_types
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|lt| lt.handle == h)
|
|
|
|
|
.map(|lt| lt.name.clone())
|
|
|
|
|
.unwrap_or_else(|| "None".to_string()),
|
|
|
|
|
None => "None".to_string(),
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
let opt_ts = |h: Option<acadrust::types::Handle>| -> String {
|
|
|
|
|
match h {
|
|
|
|
|
Some(h) => doc
|
|
|
|
|
.text_styles
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|t| t.handle == h)
|
|
|
|
|
.map(|t| t.name.clone())
|
|
|
|
|
.unwrap_or_else(|| "None".to_string()),
|
|
|
|
|
None => "None".to_string(),
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
let (line_type_name, arrowhead_name, text_style_name, block_content_name) =
|
|
|
|
|
match selected_style {
|
|
|
|
|
Some(s) => (
|
|
|
|
|
opt_lt(s.line_type_handle),
|
|
|
|
|
opt_block(s.arrowhead_handle),
|
|
|
|
|
opt_ts(s.text_style_handle),
|
|
|
|
|
opt_block(s.block_content_handle),
|
|
|
|
|
),
|
|
|
|
|
None => Default::default(),
|
|
|
|
|
};
|
|
|
|
|
sized(
|
|
|
|
|
crate::ui::mleaderstyle::view_window(crate::ui::mleaderstyle::MLeaderStyleView {
|
|
|
|
|
styles,
|
|
|
|
|
selected: &self.mleaderstyle_selected,
|
|
|
|
|
style: selected_style,
|
|
|
|
|
current: tab.active_mleader_style.clone(),
|
|
|
|
|
landing_distance: &self.mls_landing_distance,
|
|
|
|
|
landing_gap: &self.mls_landing_gap,
|
|
|
|
|
arrowhead_size: &self.mls_arrowhead_size,
|
|
|
|
|
text_height: &self.mls_text_height,
|
|
|
|
|
scale_factor: &self.mls_scale_factor,
|
|
|
|
|
break_gap: &self.mls_break_gap,
|
|
|
|
|
first_seg_angle: &self.mls_first_seg_angle,
|
|
|
|
|
second_seg_angle: &self.mls_second_seg_angle,
|
|
|
|
|
max_points: &self.mls_max_points,
|
|
|
|
|
default_text: &self.mls_default_text,
|
|
|
|
|
line_color: &self.mls_line_color,
|
|
|
|
|
text_color: &self.mls_text_color,
|
|
|
|
|
description: &self.mls_description,
|
|
|
|
|
line_weight: &self.mls_line_weight,
|
|
|
|
|
align_space: &self.mls_align_space,
|
|
|
|
|
block_color: &self.mls_block_color,
|
|
|
|
|
block_rotation: &self.mls_block_rotation,
|
|
|
|
|
block_scale_x: &self.mls_block_scale_x,
|
|
|
|
|
block_scale_y: &self.mls_block_scale_y,
|
|
|
|
|
block_scale_z: &self.mls_block_scale_z,
|
|
|
|
|
block_opts,
|
|
|
|
|
lt_opts,
|
|
|
|
|
textstyle_opts,
|
|
|
|
|
line_type_name,
|
|
|
|
|
arrowhead_name,
|
|
|
|
|
text_style_name,
|
|
|
|
|
block_content_name,
|
|
|
|
|
rename_active: self.style_rename.as_deref(),
|
|
|
|
|
rename_buf: &self.style_rename_buf,
|
|
|
|
|
color_open: self.mls_color_open,
|
|
|
|
|
}),
|
|
|
|
|
560,
|
|
|
|
|
560,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
super::ModalKind::DimStyle => {
|
|
|
|
|
|
|
|
|
|
let tab = &self.tabs[self.active_tab];
|
|
|
|
|
let styles: Vec<String> = tab
|
|
|
|
|
.scene
|
|
|
|
|
.document
|
|
|
|
|
.dim_styles
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|s| s.name.clone())
|
|
|
|
|
.collect();
|
|
|
|
|
let doc = &tab.scene.document;
|
|
|
|
|
// Dropdown options (names must match the records exactly so the
|
|
|
|
|
// selection can be resolved back to a handle on the update side).
|
|
|
|
|
let mut block_opts: Vec<String> = vec!["Default".to_string()];
|
|
|
|
|
block_opts.extend(doc.block_records.iter().map(|b| b.name.clone()));
|
|
|
|
|
let mut lt_opts: Vec<String> = vec!["ByBlock".to_string()];
|
|
|
|
|
lt_opts.extend(doc.line_types.iter().map(|lt| lt.name.clone()));
|
|
|
|
|
let blk_name = |h: acadrust::types::Handle| -> String {
|
|
|
|
|
if h.is_null() {
|
|
|
|
|
"Default".to_string()
|
|
|
|
|
} else {
|
|
|
|
|
doc.block_records
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|b| b.handle == h)
|
|
|
|
|
.map(|b| b.name.clone())
|
|
|
|
|
.unwrap_or_else(|| "Default".to_string())
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
let lt_name = |h: acadrust::types::Handle| -> String {
|
|
|
|
|
if h.is_null() {
|
|
|
|
|
"ByBlock".to_string()
|
|
|
|
|
} else {
|
|
|
|
|
doc.line_types
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|lt| lt.handle == h)
|
|
|
|
|
.map(|lt| lt.name.clone())
|
|
|
|
|
.unwrap_or_else(|| "ByBlock".to_string())
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
let ds_sel = doc.dim_styles.get(&self.dimstyle_selected);
|
|
|
|
|
let (
|
|
|
|
|
dimblk_name,
|
|
|
|
|
dimblk1_name,
|
|
|
|
|
dimblk2_name,
|
|
|
|
|
dimldrblk_name,
|
|
|
|
|
dimltex_name,
|
|
|
|
|
dimltex1_name,
|
|
|
|
|
dimltex2_name,
|
|
|
|
|
) = match ds_sel {
|
|
|
|
|
Some(d) => (
|
|
|
|
|
blk_name(d.dimblk),
|
|
|
|
|
blk_name(d.dimblk1),
|
|
|
|
|
blk_name(d.dimblk2),
|
|
|
|
|
blk_name(d.dimldrblk),
|
|
|
|
|
lt_name(d.dimltex_handle),
|
|
|
|
|
lt_name(d.dimltex1_handle),
|
|
|
|
|
lt_name(d.dimltex2_handle),
|
|
|
|
|
),
|
|
|
|
|
None => Default::default(),
|
|
|
|
|
};
|
|
|
|
|
sized(crate::ui::dimstyle::view_window(
|
|
|
|
|
styles,
|
|
|
|
|
&self.dimstyle_selected,
|
|
|
|
|
&self.tabs[self.active_tab]
|
|
|
|
|
.scene
|
|
|
|
|
.document
|
|
|
|
|
.header
|
|
|
|
|
.current_dimstyle_name,
|
|
|
|
|
self.dimstyle_tab,
|
|
|
|
|
crate::ui::dimstyle::DimStyleValues {
|
|
|
|
|
dimdle: &self.ds_dimdle,
|
|
|
|
|
dimdli: &self.ds_dimdli,
|
|
|
|
|
dimgap: &self.ds_dimgap,
|
|
|
|
|
dimexe: &self.ds_dimexe,
|
|
|
|
|
dimexo: &self.ds_dimexo,
|
|
|
|
|
dimsd1: self.ds_dimsd1,
|
|
|
|
|
dimsd2: self.ds_dimsd2,
|
|
|
|
|
dimse1: self.ds_dimse1,
|
|
|
|
|
dimse2: self.ds_dimse2,
|
|
|
|
|
dimasz: &self.ds_dimasz,
|
|
|
|
|
dimcen: &self.ds_dimcen,
|
|
|
|
|
dimtsz: &self.ds_dimtsz,
|
|
|
|
|
dimtxt: &self.ds_dimtxt,
|
|
|
|
|
dimtxsty: &self.ds_dimtxsty,
|
|
|
|
|
dimtad: &self.ds_dimtad,
|
|
|
|
|
dimtih: self.ds_dimtih,
|
|
|
|
|
dimtoh: self.ds_dimtoh,
|
|
|
|
|
dimscale: &self.ds_dimscale,
|
|
|
|
|
dimlfac: &self.ds_dimlfac,
|
|
|
|
|
dimlunit: &self.ds_dimlunit,
|
|
|
|
|
dimdec: &self.ds_dimdec,
|
|
|
|
|
dimpost: &self.ds_dimpost,
|
|
|
|
|
dimtol: self.ds_dimtol,
|
|
|
|
|
dimlim: self.ds_dimlim,
|
|
|
|
|
dimtp: &self.ds_dimtp,
|
|
|
|
|
dimtm: &self.ds_dimtm,
|
|
|
|
|
dimtdec: &self.ds_dimtdec,
|
|
|
|
|
dimtfac: &self.ds_dimtfac,
|
|
|
|
|
annotative: self.ds_annotative,
|
|
|
|
|
dimclrd: &self.ds_dimclrd,
|
|
|
|
|
dimlwd: &self.ds_dimlwd,
|
|
|
|
|
dimclre: &self.ds_dimclre,
|
|
|
|
|
dimlwe: &self.ds_dimlwe,
|
|
|
|
|
dimfxl: &self.ds_dimfxl,
|
|
|
|
|
dimfxlon: self.ds_dimfxlon,
|
|
|
|
|
dimsah: self.ds_dimsah,
|
|
|
|
|
dimarcsym: &self.ds_dimarcsym,
|
|
|
|
|
dimjogang: &self.ds_dimjogang,
|
|
|
|
|
dimclrt: &self.ds_dimclrt,
|
|
|
|
|
dimjust: &self.ds_dimjust,
|
|
|
|
|
dimtvp: &self.ds_dimtvp,
|
|
|
|
|
dimtfill: &self.ds_dimtfill,
|
|
|
|
|
dimtfillclr: &self.ds_dimtfillclr,
|
|
|
|
|
dimtxtdirection: self.ds_dimtxtdirection,
|
|
|
|
|
dimatfit: &self.ds_dimatfit,
|
|
|
|
|
dimtix: self.ds_dimtix,
|
|
|
|
|
dimsoxd: self.ds_dimsoxd,
|
|
|
|
|
dimtmove: &self.ds_dimtmove,
|
|
|
|
|
dimupt: self.ds_dimupt,
|
|
|
|
|
dimtofl: self.ds_dimtofl,
|
|
|
|
|
dimfit: &self.ds_dimfit,
|
|
|
|
|
dimdsep: &self.ds_dimdsep,
|
|
|
|
|
dimrnd: &self.ds_dimrnd,
|
|
|
|
|
dimzin: &self.ds_dimzin,
|
|
|
|
|
dimfrac: &self.ds_dimfrac,
|
|
|
|
|
dimaunit: &self.ds_dimaunit,
|
|
|
|
|
dimadec: &self.ds_dimadec,
|
|
|
|
|
dimunit: &self.ds_dimunit,
|
|
|
|
|
dimazin: &self.ds_dimazin,
|
|
|
|
|
dimalt: self.ds_dimalt,
|
|
|
|
|
dimaltf: &self.ds_dimaltf,
|
|
|
|
|
dimaltd: &self.ds_dimaltd,
|
|
|
|
|
dimaltu: &self.ds_dimaltu,
|
|
|
|
|
dimalttd: &self.ds_dimalttd,
|
|
|
|
|
dimaltrnd: &self.ds_dimaltrnd,
|
|
|
|
|
dimapost: &self.ds_dimapost,
|
|
|
|
|
dimaltz: &self.ds_dimaltz,
|
|
|
|
|
dimalttz: &self.ds_dimalttz,
|
|
|
|
|
dimtolj: &self.ds_dimtolj,
|
|
|
|
|
dimtzin: &self.ds_dimtzin,
|
|
|
|
|
dimblk_name,
|
|
|
|
|
dimblk1_name,
|
|
|
|
|
dimblk2_name,
|
|
|
|
|
dimldrblk_name,
|
|
|
|
|
dimltex_name,
|
|
|
|
|
dimltex1_name,
|
|
|
|
|
dimltex2_name,
|
|
|
|
|
block_opts,
|
|
|
|
|
lt_opts,
|
|
|
|
|
color_open: self.ds_color_open.clone(),
|
|
|
|
|
},
|
|
|
|
|
self.style_rename.as_deref(),
|
|
|
|
|
&self.style_rename_buf,
|
|
|
|
|
), 720, 560)
|
|
|
|
|
}
|
2026-06-16 23:59:38 +03:00
|
|
|
super::ModalKind::AssocPrompt => sized(default_assoc_dialog_window(), 440, 210),
|
|
|
|
|
super::ModalKind::Unsaved => {
|
|
|
|
|
let tab_name = match &self.pending_close {
|
|
|
|
|
Some(super::PendingClose::Tab(idx)) => self
|
|
|
|
|
.tabs
|
|
|
|
|
.get(*idx)
|
|
|
|
|
.map(|t| t.tab_display_name())
|
|
|
|
|
.unwrap_or_default(),
|
|
|
|
|
Some(super::PendingClose::Quit) => self
|
|
|
|
|
.tabs
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|t| t.dirty)
|
|
|
|
|
.map(|t| t.tab_display_name())
|
|
|
|
|
.unwrap_or_default(),
|
|
|
|
|
None => String::new(),
|
|
|
|
|
};
|
|
|
|
|
sized(unsaved_changes_dialog_window(&tab_name), 420, 160)
|
|
|
|
|
}
|
2026-06-17 11:13:12 +03:00
|
|
|
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,
|
|
|
|
|
),
|
2026-06-16 23:59:38 +03:00
|
|
|
super::ModalKind::SaveDialog => sized(
|
|
|
|
|
save_as_dialog_window(
|
|
|
|
|
&self.save_dialog_filename,
|
|
|
|
|
&self.save_dialog_folder,
|
|
|
|
|
&self.save_dialog_entries,
|
|
|
|
|
&self.save_dialog_format,
|
|
|
|
|
),
|
|
|
|
|
560,
|
|
|
|
|
480,
|
|
|
|
|
),
|
2026-06-16 23:22:54 +03:00
|
|
|
})
|
2026-03-22 22:44:32 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn subscription(&self) -> Subscription<Message> {
|
|
|
|
|
use iced::event;
|
2026-05-19 13:42:03 +03:00
|
|
|
// Only request per-frame ticks while something on screen is animating
|
|
|
|
|
// (currently just the open-progress indicator). Without this gate the
|
|
|
|
|
// app burned 2-3% CPU continuously redrawing an unchanged view.
|
|
|
|
|
// See #18.
|
|
|
|
|
let needs_frames = self.opening.is_some();
|
|
|
|
|
let frames = if needs_frames {
|
|
|
|
|
window::frames().map(Message::Tick)
|
|
|
|
|
} else {
|
|
|
|
|
Subscription::none()
|
|
|
|
|
};
|
2026-05-26 19:28:09 +03:00
|
|
|
// While the command-line overlay is still displaying any
|
|
|
|
|
// recently-pushed history entry, re-render every frame so the
|
|
|
|
|
// entry disappears at the moment its visible window expires.
|
|
|
|
|
// The subscription auto-stops once no entry is fresh enough
|
|
|
|
|
// (typically within a few seconds of the last command).
|
|
|
|
|
let history_tick = if self.command_line.has_visible_history() {
|
|
|
|
|
window::frames().map(Message::Tick)
|
|
|
|
|
} else {
|
|
|
|
|
Subscription::none()
|
|
|
|
|
};
|
2026-05-29 09:38:37 +03:00
|
|
|
// While the cursor sits over a grip, request animation frames
|
|
|
|
|
// so the multi-functional popup opens even when the user keeps
|
|
|
|
|
// the mouse perfectly still — `ViewportMove` alone would never
|
|
|
|
|
// fire again. Auto-stops once the hover clears or the popup is
|
|
|
|
|
// already open.
|
|
|
|
|
let grip_dwell = if self.grip_hover.is_some() && self.grip_popup.is_none() {
|
|
|
|
|
window::frames().map(|_| Message::GripDwellTick)
|
|
|
|
|
} else {
|
|
|
|
|
Subscription::none()
|
|
|
|
|
};
|
2026-06-20 20:43:33 +03:00
|
|
|
// While a rollover pick is queued, drive ticks so it fires the
|
|
|
|
|
// moment the cursor has been still for the dwell window — without
|
|
|
|
|
// this `ViewportMove` alone never re-fires once the user stops.
|
|
|
|
|
let hover_dwell = if self.hover_dwell.is_some() {
|
|
|
|
|
window::frames().map(|_| Message::HoverDwellTick)
|
|
|
|
|
} else {
|
|
|
|
|
Subscription::none()
|
|
|
|
|
};
|
2026-05-31 13:14:35 +03:00
|
|
|
// Blink the MText preview caret while the editor is open.
|
|
|
|
|
let caret_blink = if self.mtext_editor.is_some() {
|
|
|
|
|
iced::time::every(std::time::Duration::from_millis(530))
|
|
|
|
|
.map(|_| Message::MTextCaretBlink)
|
|
|
|
|
} else {
|
|
|
|
|
Subscription::none()
|
|
|
|
|
};
|
feat(web): lazy per-script font loading for international CAD text
The web build has no system-font access, so CAD text in non-Latin scripts
(Cyrillic, Greek, CJK, …) rendered as nothing — build_fallback was a hard
None on wasm (#141). Instead, ship per-script Noto subsets under web/fonts
(one alphabet per file, SIL OFL 1.1) and fetch them lazily over HTTP the
first time a drawing uses that script, then outline glyphs with ttf-parser.
- web_font: Script enum + script_of (char → script font), a per-script
store with lazy fetch, and a pending queue drained by the app loop.
- CJK is split by language (chinese/japanese/korean). Han ideographs share
code points but differ by language, so the shared block is routed by the
document's $DWGCODEPAGE (932→JP, 949→KR, GB/936→CN); kana is always
Japanese, Hangul always Korean. Re-tessellates when the language changes.
- build_fallback (wasm) outlines from the fetched subset; clear_fallback_cache
lets glyphs that missed while a font was in flight reappear on load.
- PollWebFonts subscription (web only) + WebFontLoaded message drive fetches.
- Trunk copy-dir serves web/fonts; nothing is bundled into the native binary
(desktop keeps using system fonts).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 16:13:26 +03:00
|
|
|
// Web: poll for per-script fonts that a drawing's text needs but hasn't
|
|
|
|
|
// fetched yet. Cheap — `PollWebFonts` is a no-op when nothing is
|
|
|
|
|
// pending. Native has system fonts, so no polling. (#141)
|
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
|
|
|
let web_fonts =
|
|
|
|
|
iced::time::every(std::time::Duration::from_millis(300)).map(|_| Message::PollWebFonts);
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
let web_fonts = Subscription::none();
|
2026-03-22 22:44:32 +03:00
|
|
|
iced::Subscription::batch([
|
2026-05-19 13:42:03 +03:00
|
|
|
frames,
|
2026-05-26 19:28:09 +03:00
|
|
|
history_tick,
|
2026-05-29 09:38:37 +03:00
|
|
|
grip_dwell,
|
2026-06-20 20:43:33 +03:00
|
|
|
hover_dwell,
|
2026-05-31 13:14:35 +03:00
|
|
|
caret_blink,
|
feat(web): lazy per-script font loading for international CAD text
The web build has no system-font access, so CAD text in non-Latin scripts
(Cyrillic, Greek, CJK, …) rendered as nothing — build_fallback was a hard
None on wasm (#141). Instead, ship per-script Noto subsets under web/fonts
(one alphabet per file, SIL OFL 1.1) and fetch them lazily over HTTP the
first time a drawing uses that script, then outline glyphs with ttf-parser.
- web_font: Script enum + script_of (char → script font), a per-script
store with lazy fetch, and a pending queue drained by the app loop.
- CJK is split by language (chinese/japanese/korean). Han ideographs share
code points but differ by language, so the shared block is routed by the
document's $DWGCODEPAGE (932→JP, 949→KR, GB/936→CN); kana is always
Japanese, Hangul always Korean. Re-tessellates when the language changes.
- build_fallback (wasm) outlines from the fetched subset; clear_fallback_cache
lets glyphs that missed while a font was in flight reappear on load.
- PollWebFonts subscription (web only) + WebFontLoaded message drive fetches.
- Trunk copy-dir serves web/fonts; nothing is bundled into the native binary
(desktop keeps using system fonts).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 16:13:26 +03:00
|
|
|
web_fonts,
|
2026-03-22 22:44:32 +03:00
|
|
|
event::listen_with(|ev, status, win_id| {
|
|
|
|
|
use iced::event::Status;
|
|
|
|
|
match ev {
|
2026-05-01 11:31:11 +03:00
|
|
|
iced::Event::Window(window::Event::CloseRequested) => {
|
|
|
|
|
Some(Message::WindowCloseRequested(win_id))
|
|
|
|
|
}
|
2026-03-22 22:44:32 +03:00
|
|
|
iced::Event::Window(window::Event::Closed) => {
|
|
|
|
|
Some(Message::OsWindowClosed(win_id))
|
|
|
|
|
}
|
|
|
|
|
iced::Event::Window(window::Event::Resized(sz)) => {
|
|
|
|
|
Some(Message::WindowResized(sz.width as f32, sz.height as f32))
|
|
|
|
|
}
|
2026-05-30 21:54:35 +03:00
|
|
|
iced::Event::Keyboard(keyboard::Event::ModifiersChanged(m)) => {
|
|
|
|
|
Some(Message::SetShiftDown(m.shift()))
|
|
|
|
|
}
|
2026-03-22 22:44:32 +03:00
|
|
|
iced::Event::Keyboard(keyboard::Event::KeyPressed {
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
key,
|
|
|
|
|
modifiers,
|
|
|
|
|
text,
|
|
|
|
|
..
|
2026-03-22 22:44:32 +03:00
|
|
|
}) => {
|
|
|
|
|
let ctrl = modifiers.control();
|
|
|
|
|
let shift = modifiers.shift();
|
2026-05-27 17:36:06 +03:00
|
|
|
// Any key that produces a printable glyph types it,
|
|
|
|
|
// even when its logical key resolves to navigation
|
|
|
|
|
// (NumLock-on Numpad8 / Numpad2 arrive as
|
|
|
|
|
// ArrowUp / ArrowDown but still carry text "8" /
|
|
|
|
|
// "2"). Checked before the Arrow / history arms so
|
|
|
|
|
// those numpad digits aren't swallowed as history
|
|
|
|
|
// navigation. Whitespace / control text (Space,
|
|
|
|
|
// Enter, Tab) falls through to the named handlers.
|
|
|
|
|
if !ctrl && status == Status::Ignored {
|
|
|
|
|
if let Some(t) = text.as_deref() {
|
|
|
|
|
if !t.is_empty()
|
|
|
|
|
&& t.chars().all(|c| !c.is_control() && !c.is_whitespace())
|
|
|
|
|
{
|
|
|
|
|
return Some(Message::CommandAppendChar(t.to_string()));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-22 22:44:32 +03:00
|
|
|
match key {
|
2026-05-31 13:14:35 +03:00
|
|
|
// Space is a literal space inside the MText preview
|
|
|
|
|
// but finalises a command otherwise; the handler
|
|
|
|
|
// decides based on editor state.
|
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::Space)
|
|
|
|
|
if status == Status::Ignored =>
|
|
|
|
|
{
|
|
|
|
|
Some(Message::CommandSpace)
|
|
|
|
|
}
|
2026-03-22 22:44:32 +03:00
|
|
|
keyboard::Key::Named(keyboard::key::Named::Enter)
|
|
|
|
|
if status == Status::Ignored =>
|
|
|
|
|
{
|
|
|
|
|
Some(Message::CommandFinalize)
|
|
|
|
|
}
|
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::Escape) => {
|
|
|
|
|
Some(Message::CommandEscape)
|
|
|
|
|
}
|
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::Delete)
|
|
|
|
|
if status == Status::Ignored =>
|
|
|
|
|
{
|
|
|
|
|
Some(Message::DeleteSelected)
|
|
|
|
|
}
|
2026-05-26 18:59:51 +03:00
|
|
|
keyboard::Key::Named(keyboard::key::Named::Backspace)
|
|
|
|
|
if status == Status::Ignored =>
|
|
|
|
|
{
|
|
|
|
|
Some(Message::CommandBackspace)
|
|
|
|
|
}
|
feat(dyn-input): TAB-editable distance/angle/coordinate fields
Dinamik giriş artık cursor yanında düzenlenebilir kutular gösteriyor.
TAB ile kutular arası geçilir, bir kutuya yazınca o kutu kilitlenir
(değer sabitlenir), diğerleri cursor'ı takip etmeye devam eder, Enter
ile çözülen nokta komuta point-pick olarak iletilir.
- DocumentTab.dyn_fields / dyn_active state; DynComponent + DynFieldEntry
- sync_dyn_fields komutun dyn_field() + base noktasına göre kutu kümesini
türetir: nokta+base → [d, açı], nokta+base yok → [X, Y], radius → [d],
açı → [açı]
- klavye yönlendirme: rakam/./-/+ aktif kutuya, TAB sonraki kutuya,
Enter try_dyn_commit, Backspace kilidi açar
- dyn_capturing iken command-line text alanı on_input'suz bırakılıyor
ki focus'u ve tuşları çalmasın; girişler global key aboneliğinden
geliyor (numpad dahil text alanı üzerinden yakalanıyor)
- çoklu-kutu overlay render: aktif kutu vurgulu, kilitli kutu farklı
kenar rengi
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:08:57 +03:00
|
|
|
keyboard::Key::Named(keyboard::key::Named::Tab)
|
|
|
|
|
if status == Status::Ignored =>
|
|
|
|
|
{
|
|
|
|
|
Some(Message::DynTabNext)
|
|
|
|
|
}
|
2026-05-31 10:16:30 +03:00
|
|
|
keyboard::Key::Named(keyboard::key::Named::ArrowUp)
|
|
|
|
|
if status == Status::Ignored =>
|
|
|
|
|
{
|
2026-04-07 00:04:12 +03:00
|
|
|
Some(Message::CommandHistoryPrev)
|
|
|
|
|
}
|
2026-05-31 10:16:30 +03:00
|
|
|
keyboard::Key::Named(keyboard::key::Named::ArrowDown)
|
|
|
|
|
if status == Status::Ignored =>
|
|
|
|
|
{
|
2026-04-07 00:04:12 +03:00
|
|
|
Some(Message::CommandHistoryNext)
|
|
|
|
|
}
|
2026-05-31 10:16:30 +03:00
|
|
|
// Caret movement in the MText preview (no-op
|
|
|
|
|
// otherwise; these arrows are unused elsewhere).
|
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::ArrowLeft)
|
|
|
|
|
if status == Status::Ignored =>
|
|
|
|
|
{
|
|
|
|
|
Some(Message::MTextCaretMove(-1))
|
|
|
|
|
}
|
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::ArrowRight)
|
|
|
|
|
if status == Status::Ignored =>
|
|
|
|
|
{
|
|
|
|
|
Some(Message::MTextCaretMove(1))
|
|
|
|
|
}
|
2026-03-22 22:44:32 +03:00
|
|
|
keyboard::Key::Named(keyboard::key::Named::F3) => {
|
|
|
|
|
Some(Message::ToggleSnapEnabled)
|
|
|
|
|
}
|
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::F7) => {
|
|
|
|
|
Some(Message::ToggleGrid)
|
|
|
|
|
}
|
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::F8) => {
|
|
|
|
|
Some(Message::ToggleOrtho)
|
|
|
|
|
}
|
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::F9) => {
|
|
|
|
|
Some(Message::ToggleGridSnap)
|
|
|
|
|
}
|
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::F10) => {
|
|
|
|
|
Some(Message::TogglePolar)
|
|
|
|
|
}
|
2026-04-08 00:26:54 +03:00
|
|
|
keyboard::Key::Named(keyboard::key::Named::F11) => {
|
|
|
|
|
Some(Message::ToggleOTrack)
|
|
|
|
|
}
|
2026-04-08 00:12:09 +03:00
|
|
|
keyboard::Key::Named(keyboard::key::Named::F12) => {
|
|
|
|
|
Some(Message::ToggleDynInput)
|
|
|
|
|
}
|
2026-03-22 22:44:32 +03:00
|
|
|
keyboard::Key::Character(c) if ctrl => match c.as_str() {
|
2026-05-03 19:55:38 +03:00
|
|
|
"n" => Some(Message::TabNew),
|
2026-03-22 22:44:32 +03:00
|
|
|
"o" => Some(Message::OpenFile),
|
|
|
|
|
"s" if !shift => Some(Message::SaveFile),
|
|
|
|
|
"s" if shift => Some(Message::SaveAs),
|
|
|
|
|
"z" if !shift => Some(Message::Undo),
|
|
|
|
|
"z" if shift => Some(Message::Redo),
|
|
|
|
|
"y" => Some(Message::Redo),
|
|
|
|
|
"c" => Some(Message::Command("COPYCLIP".to_string())),
|
|
|
|
|
"x" => Some(Message::Command("CUTCLIP".to_string())),
|
2026-06-20 17:05:02 +03:00
|
|
|
"v" => Some(Message::PasteShortcut),
|
2026-03-22 22:44:32 +03:00
|
|
|
_ => None,
|
|
|
|
|
},
|
2026-05-27 17:36:06 +03:00
|
|
|
// Printable glyphs are already handled by the
|
|
|
|
|
// text guard above the match; anything reaching
|
|
|
|
|
// here is a non-typing key we don't bind.
|
2026-03-22 22:44:32 +03:00
|
|
|
_ => None,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
_ => None,
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(super) fn focus_cmd_input(&self) -> Task<Message> {
|
|
|
|
|
iced::widget::operation::focus(iced::widget::Id::new(crate::ui::command_line::CMD_INPUT_ID))
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-23 02:18:41 +03:00
|
|
|
|
2026-03-22 22:44:32 +03:00
|
|
|
// ── Document tab bar ───────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
pub(super) fn doc_tab_bar<'a>(tabs: &'a [DocumentTab], active_tab: usize) -> Element<'a, Message> {
|
2026-05-12 10:36:15 +03:00
|
|
|
const BAR_BG: Color = Color {
|
|
|
|
|
r: 0.13,
|
|
|
|
|
g: 0.13,
|
|
|
|
|
b: 0.13,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
|
|
|
|
const TAB_ACTIVE: Color = Color {
|
|
|
|
|
r: 0.22,
|
|
|
|
|
g: 0.22,
|
|
|
|
|
b: 0.22,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
|
|
|
|
const TAB_HOVER: Color = Color {
|
|
|
|
|
r: 0.18,
|
|
|
|
|
g: 0.18,
|
|
|
|
|
b: 0.18,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
|
|
|
|
const TAB_INACTIVE: Color = Color {
|
|
|
|
|
r: 0.13,
|
|
|
|
|
g: 0.13,
|
|
|
|
|
b: 0.13,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
|
|
|
|
const ACCENT: Color = Color {
|
|
|
|
|
r: 0.20,
|
|
|
|
|
g: 0.55,
|
|
|
|
|
b: 0.90,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
2026-03-22 22:44:32 +03:00
|
|
|
const TEXT_ACTIVE: Color = Color::WHITE;
|
2026-05-12 10:36:15 +03:00
|
|
|
const TEXT_INACTIVE: Color = Color {
|
|
|
|
|
r: 0.60,
|
|
|
|
|
g: 0.60,
|
|
|
|
|
b: 0.60,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
|
|
|
|
const CLOSE_HOVER: Color = Color {
|
|
|
|
|
r: 0.70,
|
|
|
|
|
g: 0.22,
|
|
|
|
|
b: 0.22,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
|
|
|
|
const BORDER_COLOR: Color = Color {
|
|
|
|
|
r: 0.25,
|
|
|
|
|
g: 0.25,
|
|
|
|
|
b: 0.25,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
2026-03-22 22:44:32 +03:00
|
|
|
|
|
|
|
|
let mut bar = Row::new().spacing(0).align_y(iced::Center);
|
|
|
|
|
|
|
|
|
|
for (idx, tab) in tabs.iter().enumerate() {
|
|
|
|
|
let is_active = idx == active_tab;
|
2026-06-09 19:04:18 +03:00
|
|
|
let name = crate::ui::text_util::elide(&tab.tab_display_name(), 24);
|
2026-06-18 12:29:32 +03:00
|
|
|
let title_inner: Element<'_, Message> = if tab.dirty {
|
|
|
|
|
row![
|
|
|
|
|
crate::ui::icons::tinted(
|
|
|
|
|
crate::ui::icons::DOT,
|
|
|
|
|
7.0,
|
|
|
|
|
Color {
|
|
|
|
|
r: 0.90,
|
|
|
|
|
g: 0.75,
|
|
|
|
|
b: 0.30,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
text(name).size(12),
|
|
|
|
|
]
|
|
|
|
|
.spacing(5)
|
|
|
|
|
.align_y(iced::Center)
|
|
|
|
|
.into()
|
2026-05-12 10:36:15 +03:00
|
|
|
} else {
|
2026-06-18 12:29:32 +03:00
|
|
|
text(name).size(12).into()
|
2026-05-12 10:36:15 +03:00
|
|
|
};
|
2026-03-22 22:44:32 +03:00
|
|
|
|
2026-06-18 12:29:32 +03:00
|
|
|
let title_btn = button(title_inner)
|
2026-03-22 22:44:32 +03:00
|
|
|
.on_press(Message::TabSwitch(idx))
|
|
|
|
|
.padding([5, 12])
|
|
|
|
|
.style(move |_: &Theme, status| button::Style {
|
|
|
|
|
background: Some(Background::Color(match (is_active, status) {
|
|
|
|
|
(true, _) => TAB_ACTIVE,
|
|
|
|
|
(false, button::Status::Hovered) => TAB_HOVER,
|
|
|
|
|
_ => TAB_INACTIVE,
|
|
|
|
|
})),
|
2026-05-12 10:36:15 +03:00
|
|
|
text_color: if is_active {
|
|
|
|
|
TEXT_ACTIVE
|
|
|
|
|
} else {
|
|
|
|
|
TEXT_INACTIVE
|
|
|
|
|
},
|
2026-03-22 22:44:32 +03:00
|
|
|
border: Border {
|
2026-05-12 10:36:15 +03:00
|
|
|
color: if is_active {
|
|
|
|
|
ACCENT
|
|
|
|
|
} else {
|
|
|
|
|
Color::TRANSPARENT
|
|
|
|
|
},
|
2026-03-22 22:44:32 +03:00
|
|
|
width: if is_active { 1.0 } else { 0.0 },
|
|
|
|
|
radius: 0.0.into(),
|
|
|
|
|
},
|
|
|
|
|
shadow: iced::Shadow::default(),
|
|
|
|
|
snap: false,
|
|
|
|
|
});
|
|
|
|
|
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
// Start tab is fixed — no close button. Every other tab gets a close.
|
|
|
|
|
let row_inner: Row<'_, Message> = if tab.is_start {
|
|
|
|
|
row![title_btn].spacing(0).align_y(iced::Center)
|
|
|
|
|
} else {
|
2026-06-18 12:29:32 +03:00
|
|
|
let close_btn = button(crate::ui::icons::tinted(
|
|
|
|
|
crate::ui::icons::CLOSE,
|
|
|
|
|
10.0,
|
|
|
|
|
Color {
|
|
|
|
|
r: 0.55,
|
|
|
|
|
g: 0.55,
|
|
|
|
|
b: 0.55,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
},
|
|
|
|
|
))
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
.on_press(Message::TabClose(idx))
|
|
|
|
|
.padding([3, 5])
|
|
|
|
|
.style(move |_: &Theme, status| button::Style {
|
|
|
|
|
background: Some(Background::Color(match status {
|
|
|
|
|
button::Status::Hovered => CLOSE_HOVER,
|
|
|
|
|
_ => {
|
|
|
|
|
if is_active {
|
|
|
|
|
TAB_ACTIVE
|
|
|
|
|
} else {
|
|
|
|
|
TAB_INACTIVE
|
|
|
|
|
}
|
2026-05-12 10:36:15 +03:00
|
|
|
}
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
})),
|
|
|
|
|
border: Border {
|
|
|
|
|
radius: 3.0.into(),
|
|
|
|
|
..Default::default()
|
|
|
|
|
},
|
2026-03-22 22:44:32 +03:00
|
|
|
..Default::default()
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
});
|
|
|
|
|
row![title_btn, close_btn].spacing(0).align_y(iced::Center)
|
|
|
|
|
};
|
2026-03-22 22:44:32 +03:00
|
|
|
|
|
|
|
|
bar = bar.push(
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
container(row_inner).style(move |_: &Theme| container::Style {
|
|
|
|
|
border: Border {
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
color: if is_active {
|
|
|
|
|
BORDER_COLOR
|
|
|
|
|
} else {
|
|
|
|
|
Color::TRANSPARENT
|
|
|
|
|
},
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
width: if is_active { 1.0 } else { 0.0 },
|
|
|
|
|
radius: 0.0.into(),
|
2026-05-12 10:36:15 +03:00
|
|
|
},
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
..Default::default()
|
|
|
|
|
}),
|
2026-03-22 22:44:32 +03:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-12 10:36:15 +03:00
|
|
|
let new_btn = button(text("+").size(14).color(Color {
|
|
|
|
|
r: 0.65,
|
|
|
|
|
g: 0.65,
|
|
|
|
|
b: 0.65,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
}))
|
|
|
|
|
.on_press(Message::TabNew)
|
|
|
|
|
.padding([4, 10])
|
|
|
|
|
.style(|_: &Theme, status| button::Style {
|
|
|
|
|
background: Some(Background::Color(match status {
|
|
|
|
|
button::Status::Hovered => TAB_HOVER,
|
|
|
|
|
_ => Color::TRANSPARENT,
|
|
|
|
|
})),
|
|
|
|
|
border: Border {
|
|
|
|
|
radius: 0.0.into(),
|
2026-03-22 22:44:32 +03:00
|
|
|
..Default::default()
|
2026-05-12 10:36:15 +03:00
|
|
|
},
|
|
|
|
|
..Default::default()
|
|
|
|
|
});
|
2026-03-22 22:44:32 +03:00
|
|
|
|
|
|
|
|
bar = bar.push(new_btn);
|
|
|
|
|
bar = bar.push(iced::widget::Space::new().width(Fill));
|
|
|
|
|
|
|
|
|
|
container(bar)
|
|
|
|
|
.style(|_: &Theme| container::Style {
|
|
|
|
|
background: Some(Background::Color(BAR_BG)),
|
|
|
|
|
border: Border {
|
|
|
|
|
color: BORDER_COLOR,
|
|
|
|
|
width: 1.0,
|
|
|
|
|
radius: 0.0.into(),
|
|
|
|
|
},
|
|
|
|
|
..Default::default()
|
|
|
|
|
})
|
|
|
|
|
.height(30)
|
|
|
|
|
.width(Fill)
|
|
|
|
|
.padding([0, 2])
|
|
|
|
|
.into()
|
|
|
|
|
}
|
2026-03-31 07:13:36 +03:00
|
|
|
|
|
|
|
|
// ── Layout context-menu overlay ────────────────────────────────────────────
|
|
|
|
|
|
2026-05-29 15:58:23 +03:00
|
|
|
// ── Canvas-relative overlay positioning ────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/// Wraps `panel` in a column+row of `Space` widgets so it sits at
|
|
|
|
|
/// canvas-relative coordinates `(anchor.x, anchor.y)`. `panel` is wrapped
|
|
|
|
|
/// in `iced::widget::opaque` so mouse events on the panel itself do not
|
|
|
|
|
/// fall through to the viewport mouse area underneath; outside-click
|
|
|
|
|
/// dismissal is the caller's responsibility (handled via `ViewportLeftPress`
|
|
|
|
|
/// in `update.rs`, identical to how the multi-functional grip popup is
|
|
|
|
|
/// dismissed). Pushed into `viewport_stack` so the anchor is interpreted
|
|
|
|
|
/// in canvas-relative space, not window-relative.
|
|
|
|
|
fn position_canvas_overlay<'a>(
|
|
|
|
|
anchor: iced::Point,
|
|
|
|
|
panel: Element<'a, Message>,
|
|
|
|
|
) -> Element<'a, Message> {
|
|
|
|
|
let ax = anchor.x.max(0.0);
|
|
|
|
|
let ay = anchor.y.max(0.0);
|
|
|
|
|
column![
|
|
|
|
|
Space::new().height(iced::Length::Fixed(ay)),
|
|
|
|
|
row![
|
|
|
|
|
Space::new().width(iced::Length::Fixed(ax)),
|
|
|
|
|
iced::widget::opaque(panel),
|
|
|
|
|
],
|
|
|
|
|
]
|
|
|
|
|
.width(Fill)
|
|
|
|
|
.height(Fill)
|
|
|
|
|
.into()
|
|
|
|
|
}
|
|
|
|
|
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
// ── In-place MText editor overlay ───────────────────────────────────────────
|
|
|
|
|
|
2026-05-31 10:16:30 +03:00
|
|
|
/// Widget id for the MText editor's text area (focused when Edit mode opens).
|
|
|
|
|
pub(super) const MTEXT_TEXT_ID: &str = "mtext_editor_text";
|
|
|
|
|
|
2026-05-31 14:44:18 +03:00
|
|
|
/// Widget id for the in-place TEXT editor's input (focused when it opens).
|
|
|
|
|
pub(super) const TEXT_INLINE_ID: &str = "text_inline_input";
|
|
|
|
|
|
|
|
|
|
/// In-place single-line TEXT editor: a plain text-entry box (no formatting
|
|
|
|
|
/// toolbar), anchored at the insertion-point click. Enter commits; Esc cancels.
|
2026-06-03 10:59:59 +03:00
|
|
|
fn text_inline_overlay(
|
|
|
|
|
ed: &super::text_inline::TextInlineState,
|
|
|
|
|
canvas: (f32, f32),
|
|
|
|
|
) -> Element<'_, Message> {
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
const PANEL_BG: Color = Color {
|
|
|
|
|
r: 0.16,
|
|
|
|
|
g: 0.16,
|
|
|
|
|
b: 0.16,
|
|
|
|
|
a: 0.98,
|
|
|
|
|
};
|
|
|
|
|
const BORDER: Color = Color {
|
|
|
|
|
r: 0.40,
|
|
|
|
|
g: 0.40,
|
|
|
|
|
b: 0.40,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
2026-05-31 14:44:18 +03:00
|
|
|
|
|
|
|
|
let field = text_input("Text", &ed.value)
|
|
|
|
|
.id(iced::widget::Id::new(TEXT_INLINE_ID))
|
|
|
|
|
.on_input(Message::TextInlineInput)
|
|
|
|
|
.on_submit(Message::TextInlineOk)
|
|
|
|
|
.padding(6)
|
|
|
|
|
.size(13)
|
|
|
|
|
.width(iced::Length::Fixed(240.0));
|
|
|
|
|
|
|
|
|
|
let panel = container(field)
|
|
|
|
|
.style(move |_: &Theme| container::Style {
|
|
|
|
|
background: Some(Background::Color(PANEL_BG)),
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
border: Border {
|
|
|
|
|
color: BORDER,
|
|
|
|
|
width: 1.0,
|
|
|
|
|
radius: 5.0.into(),
|
|
|
|
|
},
|
2026-05-31 14:44:18 +03:00
|
|
|
..Default::default()
|
|
|
|
|
})
|
|
|
|
|
.padding(4);
|
|
|
|
|
|
2026-06-03 10:59:59 +03:00
|
|
|
// Keep the box on-screen so its field stays clickable at the edges.
|
|
|
|
|
const PANEL_W: f32 = 240.0 + 20.0;
|
|
|
|
|
const PANEL_H: f32 = 46.0;
|
|
|
|
|
let (cw, ch) = canvas;
|
2026-05-31 14:44:18 +03:00
|
|
|
let anchor = iced::Point::new(
|
2026-06-03 10:59:59 +03:00
|
|
|
(ed.screen_anchor.x - 6.0).clamp(0.0, (cw - PANEL_W).max(0.0)),
|
|
|
|
|
(ed.screen_anchor.y - 18.0).clamp(0.0, (ch - PANEL_H).max(0.0)),
|
2026-05-31 14:44:18 +03:00
|
|
|
);
|
|
|
|
|
position_canvas_overlay(anchor, panel.into())
|
|
|
|
|
}
|
|
|
|
|
|
feat(fonts): switch the text engine from CXF to LibreCAD LFF fonts
Replace the small QCAD CXF stroke-font set with the full LibreCAD LFF font
library (46 fonts) and a new LFF parser/engine in src/scene/lff.rs. The
parser reads the `[hex] char` glyph blocks, tessellates `,A<bulge>` arcs, and
resolves `C<hex>` glyph references. Per LibreCAD's RS_Polyline convention a
vertex's bulge curves the segment ending at that vertex — getting this wrong
turned round letters like O into sideways capsules.
Font name resolution mirrors LibreCAD: match by file stem, map common
AutoCAD/DXF SHX names (txt, isocp, complex, italic, gothic*, gost, …) to the
nearest LFF family, and fall back to `standard`. A missing glyph in the chosen
family falls back to the broad `unicode` font so accented/Cyrillic/Greek text
still renders. Cap height stays 9 units, so the text scale is unchanged.
All text consumers move from cxf:: to lff::; complex linetypes still render
embedded text but no longer draw embedded shapes (LibreCAD ships no shape
file). The CXF module and every *.cxf asset are removed, and the MText / text
style font pickers list the LFF families.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 03:31:20 +03:00
|
|
|
// Stroke-font families the renderer ships (LibreCAD LFF; see scene/lff.rs).
|
|
|
|
|
const MTEXT_FONTS: [&str; 10] = [
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
"[Style default]",
|
feat(fonts): switch the text engine from CXF to LibreCAD LFF fonts
Replace the small QCAD CXF stroke-font set with the full LibreCAD LFF font
library (46 fonts) and a new LFF parser/engine in src/scene/lff.rs. The
parser reads the `[hex] char` glyph blocks, tessellates `,A<bulge>` arcs, and
resolves `C<hex>` glyph references. Per LibreCAD's RS_Polyline convention a
vertex's bulge curves the segment ending at that vertex — getting this wrong
turned round letters like O into sideways capsules.
Font name resolution mirrors LibreCAD: match by file stem, map common
AutoCAD/DXF SHX names (txt, isocp, complex, italic, gothic*, gost, …) to the
nearest LFF family, and fall back to `standard`. A missing glyph in the chosen
family falls back to the broad `unicode` font so accented/Cyrillic/Greek text
still renders. Cap height stays 9 units, so the text scale is unchanged.
All text consumers move from cxf:: to lff::; complex linetypes still render
embedded text but no longer draw embedded shapes (LibreCAD ships no shape
file). The CXF module and every *.cxf asset are removed, and the MText / text
style font pickers list the LFF families.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 03:31:20 +03:00
|
|
|
"Standard",
|
|
|
|
|
"ISO",
|
|
|
|
|
"Simplex",
|
|
|
|
|
"RomanS",
|
|
|
|
|
"RomanD",
|
|
|
|
|
"ItalicC",
|
|
|
|
|
"ScriptS",
|
|
|
|
|
"GothGBT",
|
2026-06-16 11:18:17 +03:00
|
|
|
"RomanC",
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
];
|
|
|
|
|
/// (label, ACI). 256 = ByLayer.
|
|
|
|
|
const MTEXT_COLORS: [(&str, u16); 8] = [
|
|
|
|
|
("ByLayer", 256),
|
|
|
|
|
("Red", 1),
|
|
|
|
|
("Yellow", 2),
|
|
|
|
|
("Green", 3),
|
|
|
|
|
("Cyan", 4),
|
|
|
|
|
("Blue", 5),
|
|
|
|
|
("Magenta", 6),
|
|
|
|
|
("White", 7),
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
/// Canvas program that renders the tessellated MText strokes inside the
|
|
|
|
|
/// editor's own preview area (never on the drawing). Strokes lie in the
|
|
|
|
|
/// world XY plane; the program fits + vertically flips them into the box.
|
feat: Implement in-place MText editor with formatting toolbar and live preview
MTEXT (and double-clicking an MText) now opens an in-place editor anchored
at the insertion point instead of typing a single line on the command line.
The editor has a two-row formatting toolbar: text style / font / height,
bold, italic, underline, overline, strike, upper/lowercase and a colour
dropdown on top; oblique angle, width factor, character spacing, paragraph
alignment, justification, line spacing and OK/Cancel below. Character
toggles wrap the selection in DXF inline codes; global fields apply as
leading runs or entity properties.
Editing is shown in the editor's own area, not on the drawing: a Code/Preview
toggle switches between the raw text input and a rendered preview canvas that
draws the tessellated glyph strokes at real size, scrolling when the text
outgrows the view. The MText box keeps a fixed width so text wraps to the
next line instead of stretching into one line.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 01:51:20 +03:00
|
|
|
const MTEXT_PREVIEW_PAD: f32 = 12.0;
|
|
|
|
|
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
struct MTextPreview {
|
feat(fonts): switch the text engine from CXF to LibreCAD LFF fonts
Replace the small QCAD CXF stroke-font set with the full LibreCAD LFF font
library (46 fonts) and a new LFF parser/engine in src/scene/lff.rs. The
parser reads the `[hex] char` glyph blocks, tessellates `,A<bulge>` arcs, and
resolves `C<hex>` glyph references. Per LibreCAD's RS_Polyline convention a
vertex's bulge curves the segment ending at that vertex — getting this wrong
turned round letters like O into sideways capsules.
Font name resolution mirrors LibreCAD: match by file stem, map common
AutoCAD/DXF SHX names (txt, isocp, complex, italic, gothic*, gost, …) to the
nearest LFF family, and fall back to `standard`. A missing glyph in the chosen
family falls back to the broad `unicode` font so accented/Cyrillic/Greek text
still renders. Cap height stays 9 units, so the text scale is unchanged.
All text consumers move from cxf:: to lff::; complex linetypes still render
embedded text but no longer draw embedded shapes (LibreCAD ships no shape
file). The CXF module and every *.cxf asset are removed, and the MText / text
style font pickers list the LFF families.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 03:31:20 +03:00
|
|
|
/// Disconnected polylines as (x, y) world points + colour (NaN-split done).
|
|
|
|
|
segments: Vec<(Vec<(f32, f32)>, Color)>,
|
2026-05-31 02:33:47 +03:00
|
|
|
/// Per-visible-character boxes (world frame) for click-to-select.
|
|
|
|
|
boxes: Vec<crate::entities::text_support::GlyphBox>,
|
|
|
|
|
/// Current selection as a visible-char range.
|
|
|
|
|
sel: Option<(usize, usize)>,
|
2026-05-31 10:16:30 +03:00
|
|
|
/// Caret position as a visible-char offset.
|
|
|
|
|
caret: usize,
|
2026-05-31 13:14:35 +03:00
|
|
|
/// Whether the caret is in its visible blink phase.
|
|
|
|
|
caret_on: bool,
|
feat: Implement in-place MText editor with formatting toolbar and live preview
MTEXT (and double-clicking an MText) now opens an in-place editor anchored
at the insertion point instead of typing a single line on the command line.
The editor has a two-row formatting toolbar: text style / font / height,
bold, italic, underline, overline, strike, upper/lowercase and a colour
dropdown on top; oblique angle, width factor, character spacing, paragraph
alignment, justification, line spacing and OK/Cancel below. Character
toggles wrap the selection in DXF inline codes; global fields apply as
leading runs or entity properties.
Editing is shown in the editor's own area, not on the drawing: a Code/Preview
toggle switches between the raw text input and a rendered preview canvas that
draws the tessellated glyph strokes at real size, scrolling when the text
outgrows the view. The MText box keeps a fixed width so text wraps to the
next line instead of stretching into one line.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 01:51:20 +03:00
|
|
|
/// World-space min corner (bbox) and pixels-per-world-unit scale.
|
|
|
|
|
minx: f32,
|
|
|
|
|
miny: f32,
|
|
|
|
|
scale: f32,
|
|
|
|
|
content_h: f32,
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
}
|
|
|
|
|
|
2026-05-31 02:33:47 +03:00
|
|
|
impl MTextPreview {
|
|
|
|
|
/// Visible-char offset (0..=N) nearest the cursor point (bounds-local px).
|
|
|
|
|
fn offset_at(&self, p: iced::Point) -> usize {
|
|
|
|
|
if self.boxes.is_empty() {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
let wx = self.minx + (p.x - MTEXT_PREVIEW_PAD) / self.scale;
|
|
|
|
|
let wy = self.miny + (self.content_h - p.y - MTEXT_PREVIEW_PAD) / self.scale;
|
|
|
|
|
let mut best = 0usize;
|
|
|
|
|
let mut best_d = f32::MAX;
|
|
|
|
|
for b in &self.boxes {
|
|
|
|
|
let dx = if wx < b.xmin {
|
|
|
|
|
b.xmin - wx
|
|
|
|
|
} else if wx > b.xmax {
|
|
|
|
|
wx - b.xmax
|
|
|
|
|
} else {
|
|
|
|
|
0.0
|
|
|
|
|
};
|
|
|
|
|
let dy = if wy < b.ymin {
|
|
|
|
|
b.ymin - wy
|
|
|
|
|
} else if wy > b.ymax {
|
|
|
|
|
wy - b.ymax
|
|
|
|
|
} else {
|
|
|
|
|
0.0
|
|
|
|
|
};
|
|
|
|
|
let d = dy * 1000.0 + dx; // prefer the correct line first
|
|
|
|
|
if d < best_d {
|
|
|
|
|
best_d = d;
|
|
|
|
|
best = b.vis;
|
|
|
|
|
// After the glyph centre → caret sits after this char.
|
|
|
|
|
if wx > (b.xmin + b.xmax) * 0.5 {
|
|
|
|
|
best = b.vis + 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
best
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Default)]
|
|
|
|
|
struct MTextPreviewState {
|
|
|
|
|
dragging: bool,
|
|
|
|
|
}
|
|
|
|
|
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
impl iced::widget::canvas::Program<Message> for MTextPreview {
|
2026-05-31 02:33:47 +03:00
|
|
|
type State = MTextPreviewState;
|
|
|
|
|
|
|
|
|
|
fn update(
|
|
|
|
|
&self,
|
|
|
|
|
state: &mut MTextPreviewState,
|
|
|
|
|
event: &iced::Event,
|
|
|
|
|
bounds: iced::Rectangle,
|
|
|
|
|
cursor: iced::mouse::Cursor,
|
|
|
|
|
) -> Option<iced::widget::canvas::Action<Message>> {
|
|
|
|
|
use iced::mouse::{Button, Event as Me};
|
|
|
|
|
use iced::widget::canvas::Action;
|
|
|
|
|
use iced::Event;
|
|
|
|
|
match event {
|
|
|
|
|
Event::Mouse(Me::ButtonPressed(Button::Left)) => {
|
|
|
|
|
if let Some(p) = cursor.position_in(bounds) {
|
|
|
|
|
state.dragging = true;
|
|
|
|
|
let off = self.offset_at(p);
|
|
|
|
|
return Some(Action::publish(Message::MTextSelStart(off)).and_capture());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Event::Mouse(Me::CursorMoved { .. }) => {
|
|
|
|
|
if state.dragging {
|
|
|
|
|
if let Some(p) = cursor.position_in(bounds) {
|
|
|
|
|
let off = self.offset_at(p);
|
|
|
|
|
return Some(Action::publish(Message::MTextSelTo(off)));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Event::Mouse(Me::ButtonReleased(Button::Left)) => {
|
|
|
|
|
if state.dragging {
|
|
|
|
|
state.dragging = false;
|
|
|
|
|
return Some(Action::capture());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
fn draw(
|
|
|
|
|
&self,
|
2026-05-31 02:33:47 +03:00
|
|
|
_state: &MTextPreviewState,
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
renderer: &iced::Renderer,
|
|
|
|
|
_theme: &Theme,
|
|
|
|
|
bounds: iced::Rectangle,
|
|
|
|
|
_cursor: iced::mouse::Cursor,
|
|
|
|
|
) -> Vec<iced::widget::canvas::Geometry> {
|
|
|
|
|
use iced::widget::canvas::{Frame, Path, Stroke};
|
|
|
|
|
let mut frame = Frame::new(renderer, bounds.size());
|
feat: Implement in-place MText editor with formatting toolbar and live preview
MTEXT (and double-clicking an MText) now opens an in-place editor anchored
at the insertion point instead of typing a single line on the command line.
The editor has a two-row formatting toolbar: text style / font / height,
bold, italic, underline, overline, strike, upper/lowercase and a colour
dropdown on top; oblique angle, width factor, character spacing, paragraph
alignment, justification, line spacing and OK/Cancel below. Character
toggles wrap the selection in DXF inline codes; global fields apply as
leading runs or entity properties.
Editing is shown in the editor's own area, not on the drawing: a Code/Preview
toggle switches between the raw text input and a rendered preview canvas that
draws the tessellated glyph strokes at real size, scrolling when the text
outgrows the view. The MText box keeps a fixed width so text wraps to the
next line instead of stretching into one line.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 01:51:20 +03:00
|
|
|
let pad = MTEXT_PREVIEW_PAD;
|
|
|
|
|
// Draw at the real size; flip Y (world up → screen down).
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
let map = |x: f32, y: f32| {
|
feat: Implement in-place MText editor with formatting toolbar and live preview
MTEXT (and double-clicking an MText) now opens an in-place editor anchored
at the insertion point instead of typing a single line on the command line.
The editor has a two-row formatting toolbar: text style / font / height,
bold, italic, underline, overline, strike, upper/lowercase and a colour
dropdown on top; oblique angle, width factor, character spacing, paragraph
alignment, justification, line spacing and OK/Cancel below. Character
toggles wrap the selection in DXF inline codes; global fields apply as
leading runs or entity properties.
Editing is shown in the editor's own area, not on the drawing: a Code/Preview
toggle switches between the raw text input and a rendered preview canvas that
draws the tessellated glyph strokes at real size, scrolling when the text
outgrows the view. The MText box keeps a fixed width so text wraps to the
next line instead of stretching into one line.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 01:51:20 +03:00
|
|
|
iced::Point::new(
|
|
|
|
|
pad + (x - self.minx) * self.scale,
|
|
|
|
|
self.content_h - (pad + (y - self.miny) * self.scale),
|
|
|
|
|
)
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
};
|
2026-05-31 02:33:47 +03:00
|
|
|
// Selection highlight behind the glyphs.
|
|
|
|
|
if let Some((a, b)) = self.sel {
|
|
|
|
|
for bx in &self.boxes {
|
|
|
|
|
if bx.vis >= a && bx.vis < b {
|
|
|
|
|
let p0 = map(bx.xmin, bx.ymax);
|
|
|
|
|
let p1 = map(bx.xmax, bx.ymin);
|
|
|
|
|
let rect = Path::rectangle(
|
|
|
|
|
iced::Point::new(p0.x.min(p1.x), p0.y.min(p1.y)),
|
|
|
|
|
iced::Size::new((p1.x - p0.x).abs(), (p1.y - p0.y).abs()),
|
|
|
|
|
);
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
frame.fill(
|
|
|
|
|
&rect,
|
|
|
|
|
Color {
|
|
|
|
|
r: 0.20,
|
|
|
|
|
g: 0.42,
|
|
|
|
|
b: 0.72,
|
|
|
|
|
a: 0.45,
|
|
|
|
|
},
|
|
|
|
|
);
|
2026-05-31 02:33:47 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
feat(fonts): switch the text engine from CXF to LibreCAD LFF fonts
Replace the small QCAD CXF stroke-font set with the full LibreCAD LFF font
library (46 fonts) and a new LFF parser/engine in src/scene/lff.rs. The
parser reads the `[hex] char` glyph blocks, tessellates `,A<bulge>` arcs, and
resolves `C<hex>` glyph references. Per LibreCAD's RS_Polyline convention a
vertex's bulge curves the segment ending at that vertex — getting this wrong
turned round letters like O into sideways capsules.
Font name resolution mirrors LibreCAD: match by file stem, map common
AutoCAD/DXF SHX names (txt, isocp, complex, italic, gothic*, gost, …) to the
nearest LFF family, and fall back to `standard`. A missing glyph in the chosen
family falls back to the broad `unicode` font so accented/Cyrillic/Greek text
still renders. Cap height stays 9 units, so the text scale is unchanged.
All text consumers move from cxf:: to lff::; complex linetypes still render
embedded text but no longer draw embedded shapes (LibreCAD ships no shape
file). The CXF module and every *.cxf asset are removed, and the MText / text
style font pickers list the LFF families.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 03:31:20 +03:00
|
|
|
for (seg, col) in &self.segments {
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
if seg.len() < 2 {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
let path = Path::new(|p| {
|
|
|
|
|
p.move_to(map(seg[0].0, seg[0].1));
|
|
|
|
|
for &(x, y) in &seg[1..] {
|
|
|
|
|
p.line_to(map(x, y));
|
|
|
|
|
}
|
|
|
|
|
});
|
feat(fonts): switch the text engine from CXF to LibreCAD LFF fonts
Replace the small QCAD CXF stroke-font set with the full LibreCAD LFF font
library (46 fonts) and a new LFF parser/engine in src/scene/lff.rs. The
parser reads the `[hex] char` glyph blocks, tessellates `,A<bulge>` arcs, and
resolves `C<hex>` glyph references. Per LibreCAD's RS_Polyline convention a
vertex's bulge curves the segment ending at that vertex — getting this wrong
turned round letters like O into sideways capsules.
Font name resolution mirrors LibreCAD: match by file stem, map common
AutoCAD/DXF SHX names (txt, isocp, complex, italic, gothic*, gost, …) to the
nearest LFF family, and fall back to `standard`. A missing glyph in the chosen
family falls back to the broad `unicode` font so accented/Cyrillic/Greek text
still renders. Cap height stays 9 units, so the text scale is unchanged.
All text consumers move from cxf:: to lff::; complex linetypes still render
embedded text but no longer draw embedded shapes (LibreCAD ships no shape
file). The CXF module and every *.cxf asset are removed, and the MText / text
style font pickers list the LFF families.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 03:31:20 +03:00
|
|
|
frame.stroke(&path, Stroke::default().with_color(*col).with_width(1.4));
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
}
|
2026-05-31 10:16:30 +03:00
|
|
|
// Caret — a vertical bar at the caret's glyph boundary, shown when the
|
|
|
|
|
// selection is empty (a plain text cursor).
|
2026-05-31 13:14:35 +03:00
|
|
|
// Caret is shown only when the selection is empty and the blink is in
|
|
|
|
|
// its visible phase.
|
|
|
|
|
let collapsed = self.caret_on && self.sel.map(|(a, b)| a == b).unwrap_or(true);
|
2026-05-31 10:16:30 +03:00
|
|
|
if collapsed && self.boxes.is_empty() {
|
|
|
|
|
// Empty text: show a caret at the top-left so the user can type.
|
|
|
|
|
let path = Path::new(|p| {
|
|
|
|
|
p.move_to(iced::Point::new(MTEXT_PREVIEW_PAD, MTEXT_PREVIEW_PAD));
|
|
|
|
|
p.line_to(iced::Point::new(
|
|
|
|
|
MTEXT_PREVIEW_PAD,
|
|
|
|
|
(MTEXT_PREVIEW_PAD + 22.0).min(self.content_h),
|
|
|
|
|
));
|
|
|
|
|
});
|
|
|
|
|
frame.stroke(
|
|
|
|
|
&path,
|
|
|
|
|
Stroke::default()
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
.with_color(Color {
|
|
|
|
|
r: 0.95,
|
|
|
|
|
g: 0.95,
|
|
|
|
|
b: 0.55,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
})
|
2026-05-31 10:16:30 +03:00
|
|
|
.with_width(1.5),
|
|
|
|
|
);
|
|
|
|
|
} else if collapsed {
|
|
|
|
|
let bar = if let Some(b) = self.boxes.iter().find(|b| b.vis == self.caret) {
|
|
|
|
|
Some((b.xmin, b.ymin, b.ymax)) // left edge of the caret's glyph
|
|
|
|
|
} else if self.caret > 0 {
|
|
|
|
|
self.boxes
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|b| b.vis == self.caret - 1)
|
|
|
|
|
.map(|b| (b.xmax, b.ymin, b.ymax)) // after the last glyph
|
|
|
|
|
} else {
|
|
|
|
|
self.boxes.first().map(|b| (b.xmin, b.ymin, b.ymax))
|
|
|
|
|
};
|
|
|
|
|
if let Some((cx, y0, y1)) = bar {
|
|
|
|
|
let p0 = map(cx, y0);
|
|
|
|
|
let p1 = map(cx, y1);
|
|
|
|
|
let path = Path::new(|p| {
|
|
|
|
|
p.move_to(p0);
|
|
|
|
|
p.line_to(p1);
|
|
|
|
|
});
|
|
|
|
|
frame.stroke(
|
|
|
|
|
&path,
|
|
|
|
|
Stroke::default()
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
.with_color(Color {
|
|
|
|
|
r: 0.95,
|
|
|
|
|
g: 0.95,
|
|
|
|
|
b: 0.55,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
})
|
2026-05-31 10:16:30 +03:00
|
|
|
.with_width(1.5),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
vec![frame.into_geometry()]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
feat(fonts): switch the text engine from CXF to LibreCAD LFF fonts
Replace the small QCAD CXF stroke-font set with the full LibreCAD LFF font
library (46 fonts) and a new LFF parser/engine in src/scene/lff.rs. The
parser reads the `[hex] char` glyph blocks, tessellates `,A<bulge>` arcs, and
resolves `C<hex>` glyph references. Per LibreCAD's RS_Polyline convention a
vertex's bulge curves the segment ending at that vertex — getting this wrong
turned round letters like O into sideways capsules.
Font name resolution mirrors LibreCAD: match by file stem, map common
AutoCAD/DXF SHX names (txt, isocp, complex, italic, gothic*, gost, …) to the
nearest LFF family, and fall back to `standard`. A missing glyph in the chosen
family falls back to the broad `unicode` font so accented/Cyrillic/Greek text
still renders. Cap height stays 9 units, so the text scale is unchanged.
All text consumers move from cxf:: to lff::; complex linetypes still render
embedded text but no longer draw embedded shapes (LibreCAD ships no shape
file). The CXF module and every *.cxf asset are removed, and the MText / text
style font pickers list the LFF families.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 03:31:20 +03:00
|
|
|
/// Split every preview WireModel into finite (x, y) polyline runs, each
|
|
|
|
|
/// carrying its wire's colour so inline `\C` / the colour dropdown shows.
|
|
|
|
|
fn mtext_preview_segments(
|
|
|
|
|
ed: &super::mtext_editor::MTextEditorState,
|
|
|
|
|
) -> Vec<(Vec<(f32, f32)>, Color)> {
|
|
|
|
|
let mut out: Vec<(Vec<(f32, f32)>, Color)> = Vec::new();
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
for w in &ed.preview_wires {
|
feat(fonts): switch the text engine from CXF to LibreCAD LFF fonts
Replace the small QCAD CXF stroke-font set with the full LibreCAD LFF font
library (46 fonts) and a new LFF parser/engine in src/scene/lff.rs. The
parser reads the `[hex] char` glyph blocks, tessellates `,A<bulge>` arcs, and
resolves `C<hex>` glyph references. Per LibreCAD's RS_Polyline convention a
vertex's bulge curves the segment ending at that vertex — getting this wrong
turned round letters like O into sideways capsules.
Font name resolution mirrors LibreCAD: match by file stem, map common
AutoCAD/DXF SHX names (txt, isocp, complex, italic, gothic*, gost, …) to the
nearest LFF family, and fall back to `standard`. A missing glyph in the chosen
family falls back to the broad `unicode` font so accented/Cyrillic/Greek text
still renders. Cap height stays 9 units, so the text scale is unchanged.
All text consumers move from cxf:: to lff::; complex linetypes still render
embedded text but no longer draw embedded shapes (LibreCAD ships no shape
file). The CXF module and every *.cxf asset are removed, and the MText / text
style font pickers list the LFF families.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 03:31:20 +03:00
|
|
|
let col = Color {
|
|
|
|
|
r: w.color[0],
|
|
|
|
|
g: w.color[1],
|
|
|
|
|
b: w.color[2],
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
let mut run: Vec<(f32, f32)> = Vec::new();
|
|
|
|
|
for p in &w.points {
|
|
|
|
|
if p[0].is_finite() && p[1].is_finite() {
|
|
|
|
|
run.push((p[0], p[1]));
|
|
|
|
|
} else if !run.is_empty() {
|
feat(fonts): switch the text engine from CXF to LibreCAD LFF fonts
Replace the small QCAD CXF stroke-font set with the full LibreCAD LFF font
library (46 fonts) and a new LFF parser/engine in src/scene/lff.rs. The
parser reads the `[hex] char` glyph blocks, tessellates `,A<bulge>` arcs, and
resolves `C<hex>` glyph references. Per LibreCAD's RS_Polyline convention a
vertex's bulge curves the segment ending at that vertex — getting this wrong
turned round letters like O into sideways capsules.
Font name resolution mirrors LibreCAD: match by file stem, map common
AutoCAD/DXF SHX names (txt, isocp, complex, italic, gothic*, gost, …) to the
nearest LFF family, and fall back to `standard`. A missing glyph in the chosen
family falls back to the broad `unicode` font so accented/Cyrillic/Greek text
still renders. Cap height stays 9 units, so the text scale is unchanged.
All text consumers move from cxf:: to lff::; complex linetypes still render
embedded text but no longer draw embedded shapes (LibreCAD ships no shape
file). The CXF module and every *.cxf asset are removed, and the MText / text
style font pickers list the LFF families.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 03:31:20 +03:00
|
|
|
out.push((std::mem::take(&mut run), col));
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if !run.is_empty() {
|
feat(fonts): switch the text engine from CXF to LibreCAD LFF fonts
Replace the small QCAD CXF stroke-font set with the full LibreCAD LFF font
library (46 fonts) and a new LFF parser/engine in src/scene/lff.rs. The
parser reads the `[hex] char` glyph blocks, tessellates `,A<bulge>` arcs, and
resolves `C<hex>` glyph references. Per LibreCAD's RS_Polyline convention a
vertex's bulge curves the segment ending at that vertex — getting this wrong
turned round letters like O into sideways capsules.
Font name resolution mirrors LibreCAD: match by file stem, map common
AutoCAD/DXF SHX names (txt, isocp, complex, italic, gothic*, gost, …) to the
nearest LFF family, and fall back to `standard`. A missing glyph in the chosen
family falls back to the broad `unicode` font so accented/Cyrillic/Greek text
still renders. Cap height stays 9 units, so the text scale is unchanged.
All text consumers move from cxf:: to lff::; complex linetypes still render
embedded text but no longer draw embedded shapes (LibreCAD ships no shape
file). The CXF module and every *.cxf asset are removed, and the MText / text
style font pickers list the LFF families.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 03:31:20 +03:00
|
|
|
out.push((run, col));
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
out
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn mtext_editor_overlay<'a>(
|
|
|
|
|
ed: &'a super::mtext_editor::MTextEditorState,
|
|
|
|
|
styles: Vec<String>,
|
2026-06-03 10:59:59 +03:00
|
|
|
canvas_size: (f32, f32),
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
) -> Element<'a, Message> {
|
|
|
|
|
use super::mtext_editor::{JustifyChoice, MTextFmt, ParaAlign};
|
|
|
|
|
use iced::widget::{canvas, svg, text_editor};
|
|
|
|
|
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
const PANEL_BG: Color = Color {
|
|
|
|
|
r: 0.16,
|
|
|
|
|
g: 0.16,
|
|
|
|
|
b: 0.16,
|
|
|
|
|
a: 0.98,
|
|
|
|
|
};
|
|
|
|
|
const BORDER: Color = Color {
|
|
|
|
|
r: 0.40,
|
|
|
|
|
g: 0.40,
|
|
|
|
|
b: 0.40,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
|
|
|
|
const TEXT_COL: Color = Color {
|
|
|
|
|
r: 0.88,
|
|
|
|
|
g: 0.88,
|
|
|
|
|
b: 0.88,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
|
|
|
|
const FIELD_BG: Color = Color {
|
|
|
|
|
r: 0.12,
|
|
|
|
|
g: 0.12,
|
|
|
|
|
b: 0.12,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
|
|
|
|
|
let btn_style = |_: &Theme, status: button::Status| button::Style {
|
|
|
|
|
background: Some(Background::Color(match status {
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
button::Status::Hovered | button::Status::Pressed => Color {
|
|
|
|
|
r: 0.28,
|
|
|
|
|
g: 0.40,
|
|
|
|
|
b: 0.55,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
},
|
|
|
|
|
_ => Color {
|
|
|
|
|
r: 0.22,
|
|
|
|
|
g: 0.22,
|
|
|
|
|
b: 0.22,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
},
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
})),
|
|
|
|
|
text_color: TEXT_COL,
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
border: Border {
|
|
|
|
|
color: BORDER,
|
|
|
|
|
width: 1.0,
|
|
|
|
|
radius: 3.0.into(),
|
|
|
|
|
},
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
shadow: iced::Shadow::default(),
|
|
|
|
|
snap: false,
|
|
|
|
|
};
|
|
|
|
|
let icon_btn = move |bytes: &'static [u8], msg: Message| -> Element<'static, Message> {
|
|
|
|
|
button(svg(svg::Handle::from_memory(bytes)).width(18).height(18))
|
|
|
|
|
.on_press(msg)
|
|
|
|
|
.padding(3)
|
|
|
|
|
.style(btn_style)
|
|
|
|
|
.into()
|
|
|
|
|
};
|
|
|
|
|
let lbl = |s: &'static str| text(s).size(11).color(TEXT_COL);
|
|
|
|
|
let small_input = |placeholder: &'static str,
|
|
|
|
|
val: &str,
|
|
|
|
|
on: fn(String) -> Message,
|
|
|
|
|
w: f32|
|
|
|
|
|
-> Element<'static, Message> {
|
|
|
|
|
text_input(placeholder, val)
|
|
|
|
|
.on_input(on)
|
|
|
|
|
.width(iced::Length::Fixed(w))
|
|
|
|
|
.padding(3)
|
|
|
|
|
.size(12)
|
|
|
|
|
.into()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// ── Row 1: style / font / height · format icons · colour ──────────────
|
|
|
|
|
let style_opts: Vec<String> = if styles.is_empty() {
|
|
|
|
|
vec!["Standard".to_string()]
|
|
|
|
|
} else {
|
|
|
|
|
styles
|
|
|
|
|
};
|
|
|
|
|
let style_pl = pick_list(style_opts, Some(ed.style.clone()), Message::MTextStyle)
|
|
|
|
|
.text_size(11)
|
|
|
|
|
.width(iced::Length::Fixed(96.0));
|
|
|
|
|
let font_sel = if ed.font.trim().is_empty() {
|
|
|
|
|
"[Style default]".to_string()
|
|
|
|
|
} else {
|
|
|
|
|
ed.font.clone()
|
|
|
|
|
};
|
|
|
|
|
let font_pl = pick_list(
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
MTEXT_FONTS
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|s| s.to_string())
|
|
|
|
|
.collect::<Vec<_>>(),
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
Some(font_sel),
|
|
|
|
|
Message::MTextFont,
|
|
|
|
|
)
|
|
|
|
|
.text_size(11)
|
|
|
|
|
.width(iced::Length::Fixed(120.0));
|
|
|
|
|
let color_sel = MTEXT_COLORS
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|(_, a)| *a == ed.color_aci)
|
|
|
|
|
.map(|(n, _)| n.to_string())
|
|
|
|
|
.unwrap_or_else(|| "ByLayer".to_string());
|
|
|
|
|
let color_pl = pick_list(
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
MTEXT_COLORS
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|(n, _)| n.to_string())
|
|
|
|
|
.collect::<Vec<_>>(),
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
Some(color_sel),
|
|
|
|
|
|name: String| {
|
|
|
|
|
let aci = MTEXT_COLORS
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|(n, _)| *n == name)
|
|
|
|
|
.map(|(_, a)| *a)
|
|
|
|
|
.unwrap_or(256);
|
|
|
|
|
Message::MTextColor(aci)
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
.text_size(11)
|
|
|
|
|
.width(iced::Length::Fixed(96.0));
|
|
|
|
|
|
|
|
|
|
let row1 = row![
|
|
|
|
|
style_pl,
|
|
|
|
|
font_pl,
|
|
|
|
|
small_input("2.5", &ed.height, Message::MTextHeight, 64.0),
|
|
|
|
|
iced::widget::Space::new().width(6),
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
icon_btn(
|
|
|
|
|
include_bytes!("../../assets/icons/mt_bold.svg"),
|
|
|
|
|
Message::MTextFmt(MTextFmt::Bold)
|
|
|
|
|
),
|
|
|
|
|
icon_btn(
|
|
|
|
|
include_bytes!("../../assets/icons/mt_italic.svg"),
|
|
|
|
|
Message::MTextFmt(MTextFmt::Italic)
|
|
|
|
|
),
|
|
|
|
|
icon_btn(
|
|
|
|
|
include_bytes!("../../assets/icons/mt_underline.svg"),
|
|
|
|
|
Message::MTextFmt(MTextFmt::Underline)
|
|
|
|
|
),
|
|
|
|
|
icon_btn(
|
|
|
|
|
include_bytes!("../../assets/icons/mt_overline.svg"),
|
|
|
|
|
Message::MTextFmt(MTextFmt::Overline)
|
|
|
|
|
),
|
|
|
|
|
icon_btn(
|
|
|
|
|
include_bytes!("../../assets/icons/mt_strike.svg"),
|
|
|
|
|
Message::MTextFmt(MTextFmt::Strike)
|
|
|
|
|
),
|
|
|
|
|
icon_btn(
|
|
|
|
|
include_bytes!("../../assets/icons/mt_upper.svg"),
|
|
|
|
|
Message::MTextFmt(MTextFmt::Uppercase)
|
|
|
|
|
),
|
|
|
|
|
icon_btn(
|
|
|
|
|
include_bytes!("../../assets/icons/mt_lower.svg"),
|
|
|
|
|
Message::MTextFmt(MTextFmt::Lowercase)
|
|
|
|
|
),
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
iced::widget::Space::new().width(Fill),
|
|
|
|
|
color_pl,
|
|
|
|
|
]
|
|
|
|
|
.spacing(4)
|
|
|
|
|
.align_y(iced::Alignment::Center);
|
|
|
|
|
|
|
|
|
|
// ── Row 2: oblique / width / char-spacing · align · line spacing · OK ─
|
|
|
|
|
let justify = pick_list(
|
|
|
|
|
JustifyChoice::ALL,
|
|
|
|
|
Some(JustifyChoice(ed.attachment)),
|
|
|
|
|
|c| Message::MTextJustify(c.0),
|
|
|
|
|
)
|
|
|
|
|
.text_size(11)
|
|
|
|
|
.width(iced::Length::Fixed(112.0));
|
|
|
|
|
let row2 = row![
|
|
|
|
|
lbl("O"),
|
|
|
|
|
small_input("0", &ed.oblique, Message::MTextOblique, 48.0),
|
|
|
|
|
lbl("W"),
|
|
|
|
|
small_input("1", &ed.width, Message::MTextWidth, 48.0),
|
|
|
|
|
lbl("◊"),
|
|
|
|
|
small_input("0", &ed.char_space, Message::MTextCharSpace, 48.0),
|
|
|
|
|
iced::widget::Space::new().width(6),
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
icon_btn(
|
|
|
|
|
include_bytes!("../../assets/icons/mt_align_left.svg"),
|
|
|
|
|
Message::MTextAlign(ParaAlign::Left)
|
|
|
|
|
),
|
|
|
|
|
icon_btn(
|
|
|
|
|
include_bytes!("../../assets/icons/mt_align_center.svg"),
|
|
|
|
|
Message::MTextAlign(ParaAlign::Center)
|
|
|
|
|
),
|
|
|
|
|
icon_btn(
|
|
|
|
|
include_bytes!("../../assets/icons/mt_align_right.svg"),
|
|
|
|
|
Message::MTextAlign(ParaAlign::Right)
|
|
|
|
|
),
|
|
|
|
|
icon_btn(
|
|
|
|
|
include_bytes!("../../assets/icons/mt_align_justify.svg"),
|
|
|
|
|
Message::MTextAlign(ParaAlign::Justify)
|
|
|
|
|
),
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
iced::widget::Space::new().width(6),
|
|
|
|
|
justify,
|
|
|
|
|
lbl("LS"),
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
button(lbl("1"))
|
|
|
|
|
.on_press(Message::MTextLineSpacing(1.0))
|
|
|
|
|
.padding(3)
|
|
|
|
|
.style(btn_style),
|
|
|
|
|
button(lbl("1.5"))
|
|
|
|
|
.on_press(Message::MTextLineSpacing(1.5))
|
|
|
|
|
.padding(3)
|
|
|
|
|
.style(btn_style),
|
|
|
|
|
button(lbl("2"))
|
|
|
|
|
.on_press(Message::MTextLineSpacing(2.0))
|
|
|
|
|
.padding(3)
|
|
|
|
|
.style(btn_style),
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
iced::widget::Space::new().width(Fill),
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
icon_btn(
|
|
|
|
|
include_bytes!("../../assets/icons/mt_ok.svg"),
|
|
|
|
|
Message::MTextOk
|
|
|
|
|
),
|
|
|
|
|
icon_btn(
|
|
|
|
|
include_bytes!("../../assets/icons/mt_cancel.svg"),
|
|
|
|
|
Message::MTextCancel
|
|
|
|
|
),
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
]
|
|
|
|
|
.spacing(4)
|
|
|
|
|
.align_y(iced::Alignment::Center);
|
|
|
|
|
|
2026-05-31 01:59:39 +03:00
|
|
|
// ── Segmented Edit | Preview toggle (between toolbar and body) ────────
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
let seg_btn =
|
|
|
|
|
move |label: &'static str, active: bool, on: Message| -> Element<'static, Message> {
|
|
|
|
|
button(text(label).size(12).color(if active {
|
|
|
|
|
Color::WHITE
|
2026-05-31 01:59:39 +03:00
|
|
|
} else {
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
Color {
|
|
|
|
|
r: 0.80,
|
|
|
|
|
g: 0.80,
|
|
|
|
|
b: 0.80,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
}
|
|
|
|
|
}))
|
|
|
|
|
.on_press(on)
|
|
|
|
|
.padding([4, 14])
|
|
|
|
|
.style(move |_: &Theme, _| button::Style {
|
|
|
|
|
background: Some(Background::Color(if active {
|
|
|
|
|
Color {
|
|
|
|
|
r: 0.20,
|
|
|
|
|
g: 0.42,
|
|
|
|
|
b: 0.72,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
Color {
|
|
|
|
|
r: 0.20,
|
|
|
|
|
g: 0.20,
|
|
|
|
|
b: 0.20,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
}
|
|
|
|
|
})),
|
|
|
|
|
text_color: TEXT_COL,
|
|
|
|
|
border: Border {
|
|
|
|
|
color: BORDER,
|
|
|
|
|
width: 1.0,
|
|
|
|
|
radius: 4.0.into(),
|
|
|
|
|
},
|
|
|
|
|
shadow: iced::Shadow::default(),
|
|
|
|
|
snap: false,
|
|
|
|
|
})
|
|
|
|
|
.into()
|
|
|
|
|
};
|
2026-05-31 01:59:39 +03:00
|
|
|
let toggle = container(
|
|
|
|
|
row![
|
|
|
|
|
seg_btn("Edit", !ed.show_preview, Message::MTextShowPreview(false)),
|
|
|
|
|
seg_btn("Preview", ed.show_preview, Message::MTextShowPreview(true)),
|
|
|
|
|
]
|
|
|
|
|
.spacing(0),
|
|
|
|
|
)
|
|
|
|
|
.padding([6, 0]);
|
|
|
|
|
|
feat: Implement in-place MText editor with formatting toolbar and live preview
MTEXT (and double-clicking an MText) now opens an in-place editor anchored
at the insertion point instead of typing a single line on the command line.
The editor has a two-row formatting toolbar: text style / font / height,
bold, italic, underline, overline, strike, upper/lowercase and a colour
dropdown on top; oblique angle, width factor, character spacing, paragraph
alignment, justification, line spacing and OK/Cancel below. Character
toggles wrap the selection in DXF inline codes; global fields apply as
leading runs or entity properties.
Editing is shown in the editor's own area, not on the drawing: a Code/Preview
toggle switches between the raw text input and a rendered preview canvas that
draws the tessellated glyph strokes at real size, scrolling when the text
outgrows the view. The MText box keeps a fixed width so text wraps to the
next line instead of stretching into one line.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 01:51:20 +03:00
|
|
|
// ── Body: toggles between raw code input and rendered preview ────────
|
|
|
|
|
const VIEW_H: f32 = 150.0;
|
|
|
|
|
let body: Element<'a, Message> = if ed.show_preview {
|
|
|
|
|
let segments = mtext_preview_segments(ed);
|
|
|
|
|
let (mut minx, mut miny, mut maxx, mut maxy) = (f32::MAX, f32::MAX, f32::MIN, f32::MIN);
|
feat(fonts): switch the text engine from CXF to LibreCAD LFF fonts
Replace the small QCAD CXF stroke-font set with the full LibreCAD LFF font
library (46 fonts) and a new LFF parser/engine in src/scene/lff.rs. The
parser reads the `[hex] char` glyph blocks, tessellates `,A<bulge>` arcs, and
resolves `C<hex>` glyph references. Per LibreCAD's RS_Polyline convention a
vertex's bulge curves the segment ending at that vertex — getting this wrong
turned round letters like O into sideways capsules.
Font name resolution mirrors LibreCAD: match by file stem, map common
AutoCAD/DXF SHX names (txt, isocp, complex, italic, gothic*, gost, …) to the
nearest LFF family, and fall back to `standard`. A missing glyph in the chosen
family falls back to the broad `unicode` font so accented/Cyrillic/Greek text
still renders. Cap height stays 9 units, so the text scale is unchanged.
All text consumers move from cxf:: to lff::; complex linetypes still render
embedded text but no longer draw embedded shapes (LibreCAD ships no shape
file). The CXF module and every *.cxf asset are removed, and the MText / text
style font pickers list the LFF families.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 03:31:20 +03:00
|
|
|
for (seg, _) in &segments {
|
feat: Implement in-place MText editor with formatting toolbar and live preview
MTEXT (and double-clicking an MText) now opens an in-place editor anchored
at the insertion point instead of typing a single line on the command line.
The editor has a two-row formatting toolbar: text style / font / height,
bold, italic, underline, overline, strike, upper/lowercase and a colour
dropdown on top; oblique angle, width factor, character spacing, paragraph
alignment, justification, line spacing and OK/Cancel below. Character
toggles wrap the selection in DXF inline codes; global fields apply as
leading runs or entity properties.
Editing is shown in the editor's own area, not on the drawing: a Code/Preview
toggle switches between the raw text input and a rendered preview canvas that
draws the tessellated glyph strokes at real size, scrolling when the text
outgrows the view. The MText box keeps a fixed width so text wraps to the
next line instead of stretching into one line.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 01:51:20 +03:00
|
|
|
for &(x, y) in seg {
|
|
|
|
|
minx = minx.min(x);
|
|
|
|
|
miny = miny.min(y);
|
|
|
|
|
maxx = maxx.max(x);
|
|
|
|
|
maxy = maxy.max(y);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-31 02:33:47 +03:00
|
|
|
// Include glyph boxes so all-whitespace / box-only lines still anchor
|
|
|
|
|
// the transform (hit-testing relies on minx/miny).
|
|
|
|
|
for b in &ed.glyph_boxes {
|
|
|
|
|
minx = minx.min(b.xmin);
|
|
|
|
|
miny = miny.min(b.ymin);
|
|
|
|
|
maxx = maxx.max(b.xmax);
|
|
|
|
|
maxy = maxy.max(b.ymax);
|
|
|
|
|
}
|
feat: Implement in-place MText editor with formatting toolbar and live preview
MTEXT (and double-clicking an MText) now opens an in-place editor anchored
at the insertion point instead of typing a single line on the command line.
The editor has a two-row formatting toolbar: text style / font / height,
bold, italic, underline, overline, strike, upper/lowercase and a colour
dropdown on top; oblique angle, width factor, character spacing, paragraph
alignment, justification, line spacing and OK/Cancel below. Character
toggles wrap the selection in DXF inline codes; global fields apply as
leading runs or entity properties.
Editing is shown in the editor's own area, not on the drawing: a Code/Preview
toggle switches between the raw text input and a rendered preview canvas that
draws the tessellated glyph strokes at real size, scrolling when the text
outgrows the view. The MText box keeps a fixed width so text wraps to the
next line instead of stretching into one line.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 01:51:20 +03:00
|
|
|
let h_unit = ed.height_value() as f32;
|
|
|
|
|
// Real text size: fixed pixels per em so more/taller text grows the
|
|
|
|
|
// canvas (and scrolls) instead of shrinking to fit.
|
|
|
|
|
let scale = (22.0 / h_unit.max(1e-3)).clamp(2.0, 600.0);
|
|
|
|
|
let content_h = if maxx >= minx {
|
|
|
|
|
((maxy - miny) * scale + 2.0 * MTEXT_PREVIEW_PAD).max(40.0)
|
|
|
|
|
} else {
|
|
|
|
|
40.0
|
|
|
|
|
};
|
2026-05-31 02:33:47 +03:00
|
|
|
let prog = MTextPreview {
|
|
|
|
|
segments,
|
|
|
|
|
boxes: ed.glyph_boxes.clone(),
|
|
|
|
|
sel: ed.sel,
|
2026-05-31 10:16:30 +03:00
|
|
|
caret: ed.caret,
|
2026-05-31 13:14:35 +03:00
|
|
|
caret_on: ed.caret_blink_on,
|
2026-05-31 02:33:47 +03:00
|
|
|
minx,
|
|
|
|
|
miny,
|
|
|
|
|
scale,
|
|
|
|
|
content_h,
|
|
|
|
|
};
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
let cv = canvas(prog)
|
|
|
|
|
.width(Fill)
|
|
|
|
|
.height(iced::Length::Fixed(content_h));
|
feat: Implement in-place MText editor with formatting toolbar and live preview
MTEXT (and double-clicking an MText) now opens an in-place editor anchored
at the insertion point instead of typing a single line on the command line.
The editor has a two-row formatting toolbar: text style / font / height,
bold, italic, underline, overline, strike, upper/lowercase and a colour
dropdown on top; oblique angle, width factor, character spacing, paragraph
alignment, justification, line spacing and OK/Cancel below. Character
toggles wrap the selection in DXF inline codes; global fields apply as
leading runs or entity properties.
Editing is shown in the editor's own area, not on the drawing: a Code/Preview
toggle switches between the raw text input and a rendered preview canvas that
draws the tessellated glyph strokes at real size, scrolling when the text
outgrows the view. The MText box keeps a fixed width so text wraps to the
next line instead of stretching into one line.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 01:51:20 +03:00
|
|
|
container(iced::widget::scrollable(cv).height(iced::Length::Fixed(VIEW_H)))
|
|
|
|
|
.style(move |_: &Theme| container::Style {
|
|
|
|
|
background: Some(Background::Color(FIELD_BG)),
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
border: Border {
|
|
|
|
|
color: BORDER,
|
|
|
|
|
width: 1.0,
|
|
|
|
|
radius: 3.0.into(),
|
|
|
|
|
},
|
feat: Implement in-place MText editor with formatting toolbar and live preview
MTEXT (and double-clicking an MText) now opens an in-place editor anchored
at the insertion point instead of typing a single line on the command line.
The editor has a two-row formatting toolbar: text style / font / height,
bold, italic, underline, overline, strike, upper/lowercase and a colour
dropdown on top; oblique angle, width factor, character spacing, paragraph
alignment, justification, line spacing and OK/Cancel below. Character
toggles wrap the selection in DXF inline codes; global fields apply as
leading runs or entity properties.
Editing is shown in the editor's own area, not on the drawing: a Code/Preview
toggle switches between the raw text input and a rendered preview canvas that
draws the tessellated glyph strokes at real size, scrolling when the text
outgrows the view. The MText box keeps a fixed width so text wraps to the
next line instead of stretching into one line.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 01:51:20 +03:00
|
|
|
..Default::default()
|
|
|
|
|
})
|
|
|
|
|
.padding(2)
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
.width(Fill)
|
feat: Implement in-place MText editor with formatting toolbar and live preview
MTEXT (and double-clicking an MText) now opens an in-place editor anchored
at the insertion point instead of typing a single line on the command line.
The editor has a two-row formatting toolbar: text style / font / height,
bold, italic, underline, overline, strike, upper/lowercase and a colour
dropdown on top; oblique angle, width factor, character spacing, paragraph
alignment, justification, line spacing and OK/Cancel below. Character
toggles wrap the selection in DXF inline codes; global fields apply as
leading runs or entity properties.
Editing is shown in the editor's own area, not on the drawing: a Code/Preview
toggle switches between the raw text input and a rendered preview canvas that
draws the tessellated glyph strokes at real size, scrolling when the text
outgrows the view. The MText box keeps a fixed width so text wraps to the
next line instead of stretching into one line.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 01:51:20 +03:00
|
|
|
.into()
|
|
|
|
|
} else {
|
|
|
|
|
text_editor(&ed.content)
|
2026-05-31 10:16:30 +03:00
|
|
|
.id(iced::widget::Id::new(MTEXT_TEXT_ID))
|
feat: Implement in-place MText editor with formatting toolbar and live preview
MTEXT (and double-clicking an MText) now opens an in-place editor anchored
at the insertion point instead of typing a single line on the command line.
The editor has a two-row formatting toolbar: text style / font / height,
bold, italic, underline, overline, strike, upper/lowercase and a colour
dropdown on top; oblique angle, width factor, character spacing, paragraph
alignment, justification, line spacing and OK/Cancel below. Character
toggles wrap the selection in DXF inline codes; global fields apply as
leading runs or entity properties.
Editing is shown in the editor's own area, not on the drawing: a Code/Preview
toggle switches between the raw text input and a rendered preview canvas that
draws the tessellated glyph strokes at real size, scrolling when the text
outgrows the view. The MText box keeps a fixed width so text wraps to the
next line instead of stretching into one line.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 01:51:20 +03:00
|
|
|
.on_action(Message::MTextEdit)
|
|
|
|
|
.height(iced::Length::Fixed(VIEW_H))
|
|
|
|
|
.padding(6)
|
|
|
|
|
.size(13)
|
|
|
|
|
.into()
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-31 01:59:39 +03:00
|
|
|
let panel = container(column![row1, row2, toggle, body].spacing(5))
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
.style(move |_: &Theme| container::Style {
|
|
|
|
|
background: Some(Background::Color(PANEL_BG)),
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
border: Border {
|
|
|
|
|
color: BORDER,
|
|
|
|
|
width: 1.0,
|
|
|
|
|
radius: 5.0.into(),
|
|
|
|
|
},
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
..Default::default()
|
|
|
|
|
})
|
|
|
|
|
.padding(6)
|
|
|
|
|
.width(iced::Length::Fixed(640.0));
|
|
|
|
|
|
2026-06-03 10:59:59 +03:00
|
|
|
// Keep the whole panel on-screen: clamp the anchor so it never spills past
|
|
|
|
|
// the right/bottom edge (where its toolbar buttons would be unclickable).
|
|
|
|
|
// Width is fixed; height is the toolbar rows + the fixed VIEW_H body.
|
|
|
|
|
const PANEL_W: f32 = 640.0 + 14.0; // fixed width + padding/border
|
|
|
|
|
const PANEL_H: f32 = VIEW_H + 150.0; // body + toolbars/toggle/padding
|
|
|
|
|
let (cw, ch) = canvas_size;
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
let anchor = iced::Point::new(
|
2026-06-03 10:59:59 +03:00
|
|
|
(ed.screen_anchor.x - 10.0).clamp(0.0, (cw - PANEL_W).max(0.0)),
|
|
|
|
|
(ed.screen_anchor.y - 90.0).clamp(0.0, (ch - PANEL_H).max(0.0)),
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
);
|
|
|
|
|
position_canvas_overlay(anchor, panel.into())
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-08 00:41:01 +03:00
|
|
|
// ── Viewport right-click context menu ──────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
fn viewport_context_menu_overlay(
|
|
|
|
|
pos: iced::Point,
|
|
|
|
|
has_cmd: bool,
|
|
|
|
|
has_selection: bool,
|
2026-06-04 23:17:21 +03:00
|
|
|
isolation_active: bool,
|
2026-04-08 00:41:01 +03:00
|
|
|
last_cmds: Vec<String>,
|
feat(draworder): cross-type draw order + context menu
DRAWORDER now affects rendering immediately (bump geometry epoch so the
sort cache rebuilds) and front/back anchor to the block's full effective
sort range instead of only existing table entries, so bring-to-front
actually lands on top. The render fallback sorts table-absent entities by
their own handle.
Draw order now applies across entity types, not just within a type. A
per-entity draw-order rank (SortEntitiesTable override or own handle,
ranked per block) is encoded as a small signed clip-z bias in the wire,
hatch, image, and 3DFACE pipelines. The shared LessEqual depth buffer then
orders fills, lines, rasters and 2D solids against each other regardless of
pass order — without touching model coordinates. 3D meshes keep real
geometric depth; wipeouts stay always-on-top.
Adds a Draw Order submenu to the viewport context menu (Bring to Front,
Send to Back, Bring Above/Send Under Object). The above/under items run an
interactive reference-object pick, then reorder the captured selection.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 21:29:01 +03:00
|
|
|
draworder_open: bool,
|
2026-04-08 00:41:01 +03:00
|
|
|
) -> Element<'static, Message> {
|
2026-05-12 10:36:15 +03:00
|
|
|
const MENU_BG: Color = Color {
|
|
|
|
|
r: 0.17,
|
|
|
|
|
g: 0.17,
|
|
|
|
|
b: 0.17,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
|
|
|
|
const MENU_BORDER: Color = Color {
|
|
|
|
|
r: 0.35,
|
|
|
|
|
g: 0.35,
|
|
|
|
|
b: 0.35,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
|
|
|
|
const ITEM_HOVER: Color = Color {
|
|
|
|
|
r: 0.25,
|
|
|
|
|
g: 0.45,
|
|
|
|
|
b: 0.70,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
|
|
|
|
const TEXT_COL: Color = Color {
|
|
|
|
|
r: 0.88,
|
|
|
|
|
g: 0.88,
|
|
|
|
|
b: 0.88,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
|
|
|
|
const SEP_COL: Color = Color {
|
|
|
|
|
r: 0.30,
|
|
|
|
|
g: 0.30,
|
|
|
|
|
b: 0.30,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
2026-04-08 00:41:01 +03:00
|
|
|
|
|
|
|
|
let item = |label: String, msg: Message| -> Element<'static, Message> {
|
|
|
|
|
button(text(label).size(12).color(TEXT_COL))
|
|
|
|
|
.on_press(msg)
|
|
|
|
|
.style(|_: &Theme, status| button::Style {
|
|
|
|
|
background: Some(Background::Color(match status {
|
|
|
|
|
button::Status::Hovered | button::Status::Pressed => ITEM_HOVER,
|
|
|
|
|
_ => Color::TRANSPARENT,
|
|
|
|
|
})),
|
|
|
|
|
text_color: TEXT_COL,
|
|
|
|
|
border: Border::default(),
|
|
|
|
|
shadow: iced::Shadow::default(),
|
|
|
|
|
snap: false,
|
|
|
|
|
})
|
|
|
|
|
.padding([4, 12])
|
|
|
|
|
.width(Fill)
|
|
|
|
|
.into()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let sep = || -> Element<'static, Message> {
|
|
|
|
|
container(iced::widget::Space::new().width(Fill).height(1))
|
|
|
|
|
.style(move |_: &Theme| container::Style {
|
|
|
|
|
background: Some(Background::Color(SEP_COL)),
|
|
|
|
|
..Default::default()
|
|
|
|
|
})
|
|
|
|
|
.width(Fill)
|
|
|
|
|
.height(1)
|
|
|
|
|
.padding([0, 6])
|
|
|
|
|
.into()
|
|
|
|
|
};
|
|
|
|
|
|
feat(draworder): cross-type draw order + context menu
DRAWORDER now affects rendering immediately (bump geometry epoch so the
sort cache rebuilds) and front/back anchor to the block's full effective
sort range instead of only existing table entries, so bring-to-front
actually lands on top. The render fallback sorts table-absent entities by
their own handle.
Draw order now applies across entity types, not just within a type. A
per-entity draw-order rank (SortEntitiesTable override or own handle,
ranked per block) is encoded as a small signed clip-z bias in the wire,
hatch, image, and 3DFACE pipelines. The shared LessEqual depth buffer then
orders fills, lines, rasters and 2D solids against each other regardless of
pass order — without touching model coordinates. 3D meshes keep real
geometric depth; wipeouts stay always-on-top.
Adds a Draw Order submenu to the viewport context menu (Bring to Front,
Send to Back, Bring Above/Send Under Object). The above/under items run an
interactive reference-object pick, then reorder the captured selection.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 21:29:01 +03:00
|
|
|
// Indented variant for sub-menu rows (e.g. Draw Order children).
|
|
|
|
|
let subitem = |label: String, msg: Message| -> Element<'static, Message> {
|
|
|
|
|
button(text(label).size(12).color(TEXT_COL))
|
|
|
|
|
.on_press(msg)
|
|
|
|
|
.style(|_: &Theme, status| button::Style {
|
|
|
|
|
background: Some(Background::Color(match status {
|
|
|
|
|
button::Status::Hovered | button::Status::Pressed => ITEM_HOVER,
|
|
|
|
|
_ => Color::TRANSPARENT,
|
|
|
|
|
})),
|
|
|
|
|
text_color: TEXT_COL,
|
|
|
|
|
border: Border::default(),
|
|
|
|
|
shadow: iced::Shadow::default(),
|
|
|
|
|
snap: false,
|
|
|
|
|
})
|
|
|
|
|
.padding(iced::Padding {
|
|
|
|
|
top: 4.0,
|
|
|
|
|
right: 12.0,
|
|
|
|
|
bottom: 4.0,
|
|
|
|
|
left: 26.0,
|
|
|
|
|
})
|
|
|
|
|
.width(Fill)
|
|
|
|
|
.into()
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-08 00:41:01 +03:00
|
|
|
let mut items: Vec<Element<'static, Message>> = Vec::new();
|
|
|
|
|
|
|
|
|
|
if has_cmd {
|
|
|
|
|
items.push(item("Cancel".to_string(), Message::CommandEscape));
|
|
|
|
|
items.push(item("Enter".to_string(), Message::CommandFinalize));
|
|
|
|
|
} else {
|
|
|
|
|
if !last_cmds.is_empty() {
|
|
|
|
|
let last = last_cmds[0].clone();
|
|
|
|
|
items.push(item(
|
|
|
|
|
format!("Repeat {last}"),
|
|
|
|
|
Message::Command(last.to_uppercase()),
|
|
|
|
|
));
|
|
|
|
|
if last_cmds.len() > 1 {
|
|
|
|
|
for cmd in last_cmds.iter().skip(1) {
|
|
|
|
|
let c = cmd.clone();
|
|
|
|
|
items.push(item(c.clone(), Message::Command(c.to_uppercase())));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
items.push(sep());
|
|
|
|
|
}
|
|
|
|
|
if has_selection {
|
|
|
|
|
items.push(item("Delete".to_string(), Message::DeleteSelected));
|
2026-05-12 10:36:15 +03:00
|
|
|
items.push(item(
|
|
|
|
|
"Move".to_string(),
|
|
|
|
|
Message::Command("MOVE".to_string()),
|
|
|
|
|
));
|
|
|
|
|
items.push(item(
|
|
|
|
|
"Copy".to_string(),
|
|
|
|
|
Message::Command("COPY".to_string()),
|
|
|
|
|
));
|
2026-04-08 00:41:01 +03:00
|
|
|
items.push(sep());
|
2026-06-18 12:29:32 +03:00
|
|
|
let do_caret = if draworder_open {
|
|
|
|
|
crate::ui::icons::arrow_down(9.0, TEXT_COL)
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
} else {
|
2026-06-18 12:29:32 +03:00
|
|
|
crate::ui::icons::arrow_right(9.0, TEXT_COL)
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
};
|
2026-06-18 12:29:32 +03:00
|
|
|
items.push(
|
|
|
|
|
button(
|
|
|
|
|
row![
|
|
|
|
|
text("Draw Order").size(12).color(TEXT_COL),
|
|
|
|
|
iced::widget::Space::new().width(Fill),
|
|
|
|
|
do_caret,
|
|
|
|
|
]
|
|
|
|
|
.align_y(iced::Center),
|
|
|
|
|
)
|
|
|
|
|
.on_press(Message::DrawOrderSubmenuToggle)
|
|
|
|
|
.style(|_: &Theme, status| button::Style {
|
|
|
|
|
background: Some(Background::Color(match status {
|
|
|
|
|
button::Status::Hovered | button::Status::Pressed => ITEM_HOVER,
|
|
|
|
|
_ => Color::TRANSPARENT,
|
|
|
|
|
})),
|
|
|
|
|
text_color: TEXT_COL,
|
|
|
|
|
border: Border::default(),
|
|
|
|
|
shadow: iced::Shadow::default(),
|
|
|
|
|
snap: false,
|
|
|
|
|
})
|
|
|
|
|
.padding([4, 12])
|
|
|
|
|
.width(Fill)
|
|
|
|
|
.into(),
|
|
|
|
|
);
|
feat(draworder): cross-type draw order + context menu
DRAWORDER now affects rendering immediately (bump geometry epoch so the
sort cache rebuilds) and front/back anchor to the block's full effective
sort range instead of only existing table entries, so bring-to-front
actually lands on top. The render fallback sorts table-absent entities by
their own handle.
Draw order now applies across entity types, not just within a type. A
per-entity draw-order rank (SortEntitiesTable override or own handle,
ranked per block) is encoded as a small signed clip-z bias in the wire,
hatch, image, and 3DFACE pipelines. The shared LessEqual depth buffer then
orders fills, lines, rasters and 2D solids against each other regardless of
pass order — without touching model coordinates. 3D meshes keep real
geometric depth; wipeouts stay always-on-top.
Adds a Draw Order submenu to the viewport context menu (Bring to Front,
Send to Back, Bring Above/Send Under Object). The above/under items run an
interactive reference-object pick, then reorder the captured selection.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 21:29:01 +03:00
|
|
|
if draworder_open {
|
|
|
|
|
items.push(subitem(
|
|
|
|
|
"Bring to Front".to_string(),
|
|
|
|
|
Message::Command("DRAWORDER F".to_string()),
|
|
|
|
|
));
|
|
|
|
|
items.push(subitem(
|
|
|
|
|
"Send to Back".to_string(),
|
|
|
|
|
Message::Command("DRAWORDER B".to_string()),
|
|
|
|
|
));
|
|
|
|
|
items.push(subitem(
|
|
|
|
|
"Bring Above Object".to_string(),
|
|
|
|
|
Message::DrawOrderPickRef(true),
|
|
|
|
|
));
|
|
|
|
|
items.push(subitem(
|
|
|
|
|
"Send Under Object".to_string(),
|
|
|
|
|
Message::DrawOrderPickRef(false),
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
items.push(sep());
|
2026-06-04 23:17:21 +03:00
|
|
|
items.push(item(
|
|
|
|
|
"Isolate Objects".to_string(),
|
|
|
|
|
Message::Command("ISOLATEOBJECTS".to_string()),
|
|
|
|
|
));
|
|
|
|
|
items.push(item(
|
|
|
|
|
"Hide Objects".to_string(),
|
|
|
|
|
Message::Command("HIDEOBJECTS".to_string()),
|
|
|
|
|
));
|
|
|
|
|
items.push(sep());
|
2026-05-29 20:30:56 +03:00
|
|
|
items.push(item("Select Similar".to_string(), Message::SelectSimilar));
|
2026-06-20 21:31:05 +03:00
|
|
|
items.push(item(
|
|
|
|
|
"Invert Selection".to_string(),
|
|
|
|
|
Message::InvertSelection,
|
|
|
|
|
));
|
2026-04-08 00:41:01 +03:00
|
|
|
}
|
2026-06-04 23:17:21 +03:00
|
|
|
if isolation_active {
|
|
|
|
|
items.push(item(
|
|
|
|
|
"End Object Isolation".to_string(),
|
|
|
|
|
Message::Command("UNISOLATEOBJECTS".to_string()),
|
|
|
|
|
));
|
|
|
|
|
}
|
2026-05-12 10:36:15 +03:00
|
|
|
items.push(item(
|
|
|
|
|
"Select All".to_string(),
|
|
|
|
|
Message::Command("SELECTALL".to_string()),
|
|
|
|
|
));
|
2026-05-29 20:30:56 +03:00
|
|
|
items.push(item("Quick Select...".to_string(), Message::QSelectOpen));
|
2026-05-12 10:36:15 +03:00
|
|
|
items.push(item(
|
|
|
|
|
"Zoom Extents".to_string(),
|
2026-05-30 21:40:48 +03:00
|
|
|
Message::Command("ZOOM EXTENTS".to_string()),
|
2026-05-12 10:36:15 +03:00
|
|
|
));
|
2026-04-08 00:41:01 +03:00
|
|
|
}
|
|
|
|
|
|
2026-05-29 15:58:23 +03:00
|
|
|
let menu_col = column(items).spacing(0).width(iced::Length::Fixed(180.0));
|
2026-04-08 00:41:01 +03:00
|
|
|
|
|
|
|
|
let menu = container(menu_col)
|
|
|
|
|
.style(move |_: &Theme| container::Style {
|
|
|
|
|
background: Some(Background::Color(MENU_BG)),
|
2026-05-12 10:36:15 +03:00
|
|
|
border: Border {
|
|
|
|
|
color: MENU_BORDER,
|
|
|
|
|
width: 1.0,
|
|
|
|
|
radius: 4.0.into(),
|
|
|
|
|
},
|
2026-04-08 00:41:01 +03:00
|
|
|
..Default::default()
|
|
|
|
|
})
|
2026-05-29 15:58:23 +03:00
|
|
|
.padding([4, 0])
|
|
|
|
|
.width(iced::Length::Fixed(180.0));
|
2026-04-08 00:41:01 +03:00
|
|
|
|
2026-05-29 15:58:23 +03:00
|
|
|
position_canvas_overlay(pos, menu.into())
|
2026-04-08 00:41:01 +03:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 07:13:36 +03:00
|
|
|
/// A small right-click context menu rendered above the status bar.
|
|
|
|
|
/// The `name` is the layout tab that was right-clicked.
|
|
|
|
|
fn layout_context_menu_overlay(name: &str) -> Element<'_, Message> {
|
2026-05-12 10:36:15 +03:00
|
|
|
const MENU_BG: Color = Color {
|
|
|
|
|
r: 0.17,
|
|
|
|
|
g: 0.17,
|
|
|
|
|
b: 0.17,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
|
|
|
|
const MENU_BORDER: Color = Color {
|
|
|
|
|
r: 0.35,
|
|
|
|
|
g: 0.35,
|
|
|
|
|
b: 0.35,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
|
|
|
|
const ITEM_HOVER: Color = Color {
|
|
|
|
|
r: 0.25,
|
|
|
|
|
g: 0.45,
|
|
|
|
|
b: 0.70,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
|
|
|
|
const TEXT_COLOR: Color = Color {
|
|
|
|
|
r: 0.88,
|
|
|
|
|
g: 0.88,
|
|
|
|
|
b: 0.88,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
2026-03-31 07:13:36 +03:00
|
|
|
|
|
|
|
|
let item = |label: &'static str, msg: Message| {
|
|
|
|
|
button(text(label).size(12).color(TEXT_COLOR))
|
|
|
|
|
.on_press(msg)
|
|
|
|
|
.style(|_: &Theme, status| button::Style {
|
|
|
|
|
background: Some(Background::Color(match status {
|
|
|
|
|
button::Status::Hovered | button::Status::Pressed => ITEM_HOVER,
|
|
|
|
|
_ => Color::TRANSPARENT,
|
|
|
|
|
})),
|
|
|
|
|
text_color: TEXT_COLOR,
|
|
|
|
|
border: Border::default(),
|
|
|
|
|
shadow: iced::Shadow::default(),
|
|
|
|
|
snap: false,
|
|
|
|
|
})
|
|
|
|
|
.padding([4, 12])
|
|
|
|
|
.width(Fill)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let rename_name = name.to_string();
|
|
|
|
|
let delete_name = name.to_string();
|
|
|
|
|
|
|
|
|
|
let menu = container(
|
|
|
|
|
column![
|
2026-03-31 08:18:29 +03:00
|
|
|
item("Rename", Message::LayoutRenameStart(rename_name)),
|
|
|
|
|
item("Delete", Message::LayoutDelete(delete_name)),
|
2026-03-31 07:13:36 +03:00
|
|
|
]
|
|
|
|
|
.spacing(0)
|
|
|
|
|
.width(160),
|
|
|
|
|
)
|
|
|
|
|
.style(move |_: &Theme| container::Style {
|
|
|
|
|
background: Some(Background::Color(MENU_BG)),
|
|
|
|
|
border: Border {
|
|
|
|
|
color: MENU_BORDER,
|
|
|
|
|
width: 1.0,
|
|
|
|
|
radius: 4.0.into(),
|
|
|
|
|
},
|
|
|
|
|
..Default::default()
|
|
|
|
|
})
|
|
|
|
|
.padding([4, 0]);
|
|
|
|
|
|
|
|
|
|
// Click-catcher fills the whole screen to close the menu when clicking outside.
|
|
|
|
|
let catcher = mouse_area(
|
|
|
|
|
container(iced::widget::Space::new().width(Fill).height(Fill))
|
|
|
|
|
.width(Fill)
|
|
|
|
|
.height(Fill),
|
|
|
|
|
)
|
|
|
|
|
.on_press(Message::LayoutContextMenuClose)
|
|
|
|
|
.on_right_press(Message::LayoutContextMenuClose);
|
|
|
|
|
|
|
|
|
|
// Position the menu above the status bar at the left.
|
|
|
|
|
let positioned = container(menu)
|
|
|
|
|
.align_bottom(Fill)
|
|
|
|
|
.align_left(Fill)
|
2026-05-12 10:36:15 +03:00
|
|
|
.padding(iced::Padding {
|
|
|
|
|
top: 0.0,
|
|
|
|
|
right: 0.0,
|
|
|
|
|
bottom: 30.0,
|
|
|
|
|
left: 8.0,
|
|
|
|
|
});
|
2026-03-31 07:13:36 +03:00
|
|
|
|
|
|
|
|
stack![catcher, positioned].into()
|
|
|
|
|
}
|
2026-03-31 08:05:17 +03:00
|
|
|
|
2026-05-29 20:30:56 +03:00
|
|
|
// ── Quick Select panel ─────────────────────────────────────────────────────
|
|
|
|
|
|
feat(qselect): per-property filter with operator + value (#28)
Quick Select used to filter only on Object Type and Layer. The classic
QSELECT model — pick a type, pick one of its properties, compare with a
typed value via a logical operator — is much more general: a Line entity
can now be filtered on Start X, Length, Angle, Delta Y, …, and any
entity on the common Layer / Color / Linetype / Lineweight.
Panel layout: Object type → Property → Operator → Value → Append. The
Property dropdown is type-aware: it always lists the four common
properties; picking a specific Object type appends that type's
`geometry_properties` rows, sampled from the first entity of that type
in the active layout (so `Start X`, `Radius`, `Pattern Angle`, … only
appear when their entity type is selected). Changing the Object type
drops the Property when the new type no longer offers it.
Operators: `= Equals`, `!= Not equal`, `> Greater than`, `< Less than`,
`* Any value`. Eq/Neq do case-insensitive string compare against the
canonical property string (so "ByLayer" matches "bylayer"); Gt/Lt parse
both sides as `f64` and reject anything non-numeric; Any skips the
value test entirely. The Value text field is disabled when no Property
is picked or the operator is Any. Append behaves the same as before.
The new value reader is hand-rolled for the four common properties
(layer name, color formatted as `ByLayer`/`ByBlock`/`<aci>`/`r,g,b`,
linetype name, lineweight formatted as `0.30mm`) and falls back through
`geometry_properties()` for type-specific fields — extracting the
property's value out of the `PropValue` variant so EditText / Choice /
ColorChoice all canonicalise the same way the panel will display them.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 21:09:56 +03:00
|
|
|
const QSELECT_ANY_TYPE: &str = "(Any type)";
|
|
|
|
|
const QSELECT_ANY_PROP: &str = "(Any property)";
|
2026-05-29 20:30:56 +03:00
|
|
|
|
feat(qselect): per-property filter with operator + value (#28)
Quick Select used to filter only on Object Type and Layer. The classic
QSELECT model — pick a type, pick one of its properties, compare with a
typed value via a logical operator — is much more general: a Line entity
can now be filtered on Start X, Length, Angle, Delta Y, …, and any
entity on the common Layer / Color / Linetype / Lineweight.
Panel layout: Object type → Property → Operator → Value → Append. The
Property dropdown is type-aware: it always lists the four common
properties; picking a specific Object type appends that type's
`geometry_properties` rows, sampled from the first entity of that type
in the active layout (so `Start X`, `Radius`, `Pattern Angle`, … only
appear when their entity type is selected). Changing the Object type
drops the Property when the new type no longer offers it.
Operators: `= Equals`, `!= Not equal`, `> Greater than`, `< Less than`,
`* Any value`. Eq/Neq do case-insensitive string compare against the
canonical property string (so "ByLayer" matches "bylayer"); Gt/Lt parse
both sides as `f64` and reject anything non-numeric; Any skips the
value test entirely. The Value text field is disabled when no Property
is picked or the operator is Any. Append behaves the same as before.
The new value reader is hand-rolled for the four common properties
(layer name, color formatted as `ByLayer`/`ByBlock`/`<aci>`/`r,g,b`,
linetype name, lineweight formatted as `0.30mm`) and falls back through
`geometry_properties()` for type-specific fields — extracting the
property's value out of the `PropValue` variant so EditText / Choice /
ColorChoice all canonicalise the same way the panel will display them.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 21:09:56 +03:00
|
|
|
/// Floating panel for the Quick Select feature. Single-row filter:
|
|
|
|
|
/// object type → property → operator → value, plus an "Append to current
|
|
|
|
|
/// selection" checkbox. The property dropdown is type-aware — Common
|
|
|
|
|
/// properties (Layer, Color, Linetype, Lineweight) are always shown;
|
|
|
|
|
/// picking a specific Object type adds that type's `geometry_properties`
|
|
|
|
|
/// fields (Start X, Length, Radius, …) so type-specific filtering works.
|
2026-05-29 20:30:56 +03:00
|
|
|
fn qselect_overlay<'a>(
|
|
|
|
|
state: &'a crate::app::QSelectState,
|
|
|
|
|
types: &[&'static str],
|
feat(qselect): per-property filter with operator + value (#28)
Quick Select used to filter only on Object Type and Layer. The classic
QSELECT model — pick a type, pick one of its properties, compare with a
typed value via a logical operator — is much more general: a Line entity
can now be filtered on Start X, Length, Angle, Delta Y, …, and any
entity on the common Layer / Color / Linetype / Lineweight.
Panel layout: Object type → Property → Operator → Value → Append. The
Property dropdown is type-aware: it always lists the four common
properties; picking a specific Object type appends that type's
`geometry_properties` rows, sampled from the first entity of that type
in the active layout (so `Start X`, `Radius`, `Pattern Angle`, … only
appear when their entity type is selected). Changing the Object type
drops the Property when the new type no longer offers it.
Operators: `= Equals`, `!= Not equal`, `> Greater than`, `< Less than`,
`* Any value`. Eq/Neq do case-insensitive string compare against the
canonical property string (so "ByLayer" matches "bylayer"); Gt/Lt parse
both sides as `f64` and reject anything non-numeric; Any skips the
value test entirely. The Value text field is disabled when no Property
is picked or the operator is Any. Append behaves the same as before.
The new value reader is hand-rolled for the four common properties
(layer name, color formatted as `ByLayer`/`ByBlock`/`<aci>`/`r,g,b`,
linetype name, lineweight formatted as `0.30mm`) and falls back through
`geometry_properties()` for type-specific fields — extracting the
property's value out of the `PropValue` variant so EditText / Choice /
ColorChoice all canonicalise the same way the panel will display them.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 21:09:56 +03:00
|
|
|
properties: &[(String, String)],
|
2026-05-29 20:30:56 +03:00
|
|
|
) -> Element<'a, Message> {
|
|
|
|
|
use iced::widget::{checkbox, pick_list};
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
const BG: Color = Color {
|
|
|
|
|
r: 0.12,
|
|
|
|
|
g: 0.12,
|
|
|
|
|
b: 0.12,
|
|
|
|
|
a: 0.98,
|
|
|
|
|
};
|
|
|
|
|
const BORDER: Color = Color {
|
|
|
|
|
r: 0.35,
|
|
|
|
|
g: 0.35,
|
|
|
|
|
b: 0.35,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
|
|
|
|
const TEXT: Color = Color {
|
|
|
|
|
r: 0.88,
|
|
|
|
|
g: 0.88,
|
|
|
|
|
b: 0.88,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
|
|
|
|
const BTN_OK: Color = Color {
|
|
|
|
|
r: 0.22,
|
|
|
|
|
g: 0.42,
|
|
|
|
|
b: 0.68,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
|
|
|
|
const BTN_OK_HOV: Color = Color {
|
|
|
|
|
r: 0.30,
|
|
|
|
|
g: 0.52,
|
|
|
|
|
b: 0.80,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
|
|
|
|
const BTN_BG: Color = Color {
|
|
|
|
|
r: 0.22,
|
|
|
|
|
g: 0.22,
|
|
|
|
|
b: 0.22,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
|
|
|
|
const BTN_HOV: Color = Color {
|
|
|
|
|
r: 0.30,
|
|
|
|
|
g: 0.30,
|
|
|
|
|
b: 0.30,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
2026-05-29 20:30:56 +03:00
|
|
|
|
feat(qselect): per-property filter with operator + value (#28)
Quick Select used to filter only on Object Type and Layer. The classic
QSELECT model — pick a type, pick one of its properties, compare with a
typed value via a logical operator — is much more general: a Line entity
can now be filtered on Start X, Length, Angle, Delta Y, …, and any
entity on the common Layer / Color / Linetype / Lineweight.
Panel layout: Object type → Property → Operator → Value → Append. The
Property dropdown is type-aware: it always lists the four common
properties; picking a specific Object type appends that type's
`geometry_properties` rows, sampled from the first entity of that type
in the active layout (so `Start X`, `Radius`, `Pattern Angle`, … only
appear when their entity type is selected). Changing the Object type
drops the Property when the new type no longer offers it.
Operators: `= Equals`, `!= Not equal`, `> Greater than`, `< Less than`,
`* Any value`. Eq/Neq do case-insensitive string compare against the
canonical property string (so "ByLayer" matches "bylayer"); Gt/Lt parse
both sides as `f64` and reject anything non-numeric; Any skips the
value test entirely. The Value text field is disabled when no Property
is picked or the operator is Any. Append behaves the same as before.
The new value reader is hand-rolled for the four common properties
(layer name, color formatted as `ByLayer`/`ByBlock`/`<aci>`/`r,g,b`,
linetype name, lineweight formatted as `0.30mm`) and falls back through
`geometry_properties()` for type-specific fields — extracting the
property's value out of the `PropValue` variant so EditText / Choice /
ColorChoice all canonicalise the same way the panel will display them.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 21:09:56 +03:00
|
|
|
let mut type_options: Vec<String> = vec![QSELECT_ANY_TYPE.to_string()];
|
2026-05-29 20:30:56 +03:00
|
|
|
type_options.extend(types.iter().map(|s| (*s).to_string()));
|
feat(qselect): per-property filter with operator + value (#28)
Quick Select used to filter only on Object Type and Layer. The classic
QSELECT model — pick a type, pick one of its properties, compare with a
typed value via a logical operator — is much more general: a Line entity
can now be filtered on Start X, Length, Angle, Delta Y, …, and any
entity on the common Layer / Color / Linetype / Lineweight.
Panel layout: Object type → Property → Operator → Value → Append. The
Property dropdown is type-aware: it always lists the four common
properties; picking a specific Object type appends that type's
`geometry_properties` rows, sampled from the first entity of that type
in the active layout (so `Start X`, `Radius`, `Pattern Angle`, … only
appear when their entity type is selected). Changing the Object type
drops the Property when the new type no longer offers it.
Operators: `= Equals`, `!= Not equal`, `> Greater than`, `< Less than`,
`* Any value`. Eq/Neq do case-insensitive string compare against the
canonical property string (so "ByLayer" matches "bylayer"); Gt/Lt parse
both sides as `f64` and reject anything non-numeric; Any skips the
value test entirely. The Value text field is disabled when no Property
is picked or the operator is Any. Append behaves the same as before.
The new value reader is hand-rolled for the four common properties
(layer name, color formatted as `ByLayer`/`ByBlock`/`<aci>`/`r,g,b`,
linetype name, lineweight formatted as `0.30mm`) and falls back through
`geometry_properties()` for type-specific fields — extracting the
property's value out of the `PropValue` variant so EditText / Choice /
ColorChoice all canonicalise the same way the panel will display them.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 21:09:56 +03:00
|
|
|
|
|
|
|
|
let mut prop_options: Vec<crate::app::QSelectPropertyChoice> =
|
|
|
|
|
vec![crate::app::QSelectPropertyChoice {
|
|
|
|
|
field: String::new(),
|
|
|
|
|
label: QSELECT_ANY_PROP.to_string(),
|
|
|
|
|
}];
|
|
|
|
|
prop_options.extend(properties.iter().map(|(field, label)| {
|
|
|
|
|
crate::app::QSelectPropertyChoice {
|
|
|
|
|
field: field.clone(),
|
|
|
|
|
label: label.clone(),
|
|
|
|
|
}
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
let op_options: Vec<crate::app::QSelectOp> = vec![
|
|
|
|
|
crate::app::QSelectOp::Eq,
|
|
|
|
|
crate::app::QSelectOp::Neq,
|
|
|
|
|
crate::app::QSelectOp::Gt,
|
|
|
|
|
crate::app::QSelectOp::Lt,
|
|
|
|
|
crate::app::QSelectOp::Any,
|
|
|
|
|
];
|
2026-05-29 20:30:56 +03:00
|
|
|
|
|
|
|
|
let type_sel = state
|
|
|
|
|
.type_filter
|
|
|
|
|
.clone()
|
feat(qselect): per-property filter with operator + value (#28)
Quick Select used to filter only on Object Type and Layer. The classic
QSELECT model — pick a type, pick one of its properties, compare with a
typed value via a logical operator — is much more general: a Line entity
can now be filtered on Start X, Length, Angle, Delta Y, …, and any
entity on the common Layer / Color / Linetype / Lineweight.
Panel layout: Object type → Property → Operator → Value → Append. The
Property dropdown is type-aware: it always lists the four common
properties; picking a specific Object type appends that type's
`geometry_properties` rows, sampled from the first entity of that type
in the active layout (so `Start X`, `Radius`, `Pattern Angle`, … only
appear when their entity type is selected). Changing the Object type
drops the Property when the new type no longer offers it.
Operators: `= Equals`, `!= Not equal`, `> Greater than`, `< Less than`,
`* Any value`. Eq/Neq do case-insensitive string compare against the
canonical property string (so "ByLayer" matches "bylayer"); Gt/Lt parse
both sides as `f64` and reject anything non-numeric; Any skips the
value test entirely. The Value text field is disabled when no Property
is picked or the operator is Any. Append behaves the same as before.
The new value reader is hand-rolled for the four common properties
(layer name, color formatted as `ByLayer`/`ByBlock`/`<aci>`/`r,g,b`,
linetype name, lineweight formatted as `0.30mm`) and falls back through
`geometry_properties()` for type-specific fields — extracting the
property's value out of the `PropValue` variant so EditText / Choice /
ColorChoice all canonicalise the same way the panel will display them.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 21:09:56 +03:00
|
|
|
.unwrap_or_else(|| QSELECT_ANY_TYPE.to_string());
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
let prop_sel = state
|
|
|
|
|
.property
|
|
|
|
|
.clone()
|
|
|
|
|
.unwrap_or(crate::app::QSelectPropertyChoice {
|
|
|
|
|
field: String::new(),
|
|
|
|
|
label: QSELECT_ANY_PROP.to_string(),
|
|
|
|
|
});
|
feat(qselect): per-property filter with operator + value (#28)
Quick Select used to filter only on Object Type and Layer. The classic
QSELECT model — pick a type, pick one of its properties, compare with a
typed value via a logical operator — is much more general: a Line entity
can now be filtered on Start X, Length, Angle, Delta Y, …, and any
entity on the common Layer / Color / Linetype / Lineweight.
Panel layout: Object type → Property → Operator → Value → Append. The
Property dropdown is type-aware: it always lists the four common
properties; picking a specific Object type appends that type's
`geometry_properties` rows, sampled from the first entity of that type
in the active layout (so `Start X`, `Radius`, `Pattern Angle`, … only
appear when their entity type is selected). Changing the Object type
drops the Property when the new type no longer offers it.
Operators: `= Equals`, `!= Not equal`, `> Greater than`, `< Less than`,
`* Any value`. Eq/Neq do case-insensitive string compare against the
canonical property string (so "ByLayer" matches "bylayer"); Gt/Lt parse
both sides as `f64` and reject anything non-numeric; Any skips the
value test entirely. The Value text field is disabled when no Property
is picked or the operator is Any. Append behaves the same as before.
The new value reader is hand-rolled for the four common properties
(layer name, color formatted as `ByLayer`/`ByBlock`/`<aci>`/`r,g,b`,
linetype name, lineweight formatted as `0.30mm`) and falls back through
`geometry_properties()` for type-specific fields — extracting the
property's value out of the `PropValue` variant so EditText / Choice /
ColorChoice all canonicalise the same way the panel will display them.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 21:09:56 +03:00
|
|
|
|
|
|
|
|
// The value field is disabled (visually de-emphasised; we still
|
|
|
|
|
// render the same widget) when no property is picked or the
|
|
|
|
|
// operator is "*Any value" — both of those skip the value test.
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
let value_enabled =
|
|
|
|
|
state.property.is_some() && !matches!(state.operator, crate::app::QSelectOp::Any);
|
2026-05-29 20:30:56 +03:00
|
|
|
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
let label = |s: &'static str| {
|
|
|
|
|
text(s)
|
|
|
|
|
.size(12)
|
|
|
|
|
.color(TEXT)
|
|
|
|
|
.width(iced::Length::Fixed(90.0))
|
|
|
|
|
};
|
2026-05-29 20:30:56 +03:00
|
|
|
|
|
|
|
|
let btn = |lbl: &'static str, msg: Message, base: Color, hov: Color| {
|
|
|
|
|
button(text(lbl).size(12).color(TEXT))
|
|
|
|
|
.on_press(msg)
|
|
|
|
|
.style(move |_: &Theme, st| button::Style {
|
|
|
|
|
background: Some(Background::Color(
|
|
|
|
|
if matches!(st, button::Status::Hovered | button::Status::Pressed) {
|
|
|
|
|
hov
|
|
|
|
|
} else {
|
|
|
|
|
base
|
|
|
|
|
},
|
|
|
|
|
)),
|
|
|
|
|
text_color: TEXT,
|
|
|
|
|
border: Border {
|
|
|
|
|
color: BORDER,
|
|
|
|
|
width: 1.0,
|
|
|
|
|
radius: 4.0.into(),
|
|
|
|
|
},
|
|
|
|
|
..Default::default()
|
|
|
|
|
})
|
|
|
|
|
.padding([4, 14])
|
|
|
|
|
};
|
|
|
|
|
|
feat(qselect): per-property filter with operator + value (#28)
Quick Select used to filter only on Object Type and Layer. The classic
QSELECT model — pick a type, pick one of its properties, compare with a
typed value via a logical operator — is much more general: a Line entity
can now be filtered on Start X, Length, Angle, Delta Y, …, and any
entity on the common Layer / Color / Linetype / Lineweight.
Panel layout: Object type → Property → Operator → Value → Append. The
Property dropdown is type-aware: it always lists the four common
properties; picking a specific Object type appends that type's
`geometry_properties` rows, sampled from the first entity of that type
in the active layout (so `Start X`, `Radius`, `Pattern Angle`, … only
appear when their entity type is selected). Changing the Object type
drops the Property when the new type no longer offers it.
Operators: `= Equals`, `!= Not equal`, `> Greater than`, `< Less than`,
`* Any value`. Eq/Neq do case-insensitive string compare against the
canonical property string (so "ByLayer" matches "bylayer"); Gt/Lt parse
both sides as `f64` and reject anything non-numeric; Any skips the
value test entirely. The Value text field is disabled when no Property
is picked or the operator is Any. Append behaves the same as before.
The new value reader is hand-rolled for the four common properties
(layer name, color formatted as `ByLayer`/`ByBlock`/`<aci>`/`r,g,b`,
linetype name, lineweight formatted as `0.30mm`) and falls back through
`geometry_properties()` for type-specific fields — extracting the
property's value out of the `PropValue` variant so EditText / Choice /
ColorChoice all canonicalise the same way the panel will display them.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 21:09:56 +03:00
|
|
|
let mut value_input = text_input("", &state.value).size(12);
|
|
|
|
|
if value_enabled {
|
|
|
|
|
value_input = value_input.on_input(Message::QSelectSetValue);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-29 20:30:56 +03:00
|
|
|
let panel_body = column![
|
|
|
|
|
text("Quick Select").size(14).color(TEXT),
|
|
|
|
|
Space::new().height(10),
|
|
|
|
|
row![
|
|
|
|
|
label("Object type:"),
|
|
|
|
|
pick_list(type_options, Some(type_sel), |s: String| {
|
feat(qselect): per-property filter with operator + value (#28)
Quick Select used to filter only on Object Type and Layer. The classic
QSELECT model — pick a type, pick one of its properties, compare with a
typed value via a logical operator — is much more general: a Line entity
can now be filtered on Start X, Length, Angle, Delta Y, …, and any
entity on the common Layer / Color / Linetype / Lineweight.
Panel layout: Object type → Property → Operator → Value → Append. The
Property dropdown is type-aware: it always lists the four common
properties; picking a specific Object type appends that type's
`geometry_properties` rows, sampled from the first entity of that type
in the active layout (so `Start X`, `Radius`, `Pattern Angle`, … only
appear when their entity type is selected). Changing the Object type
drops the Property when the new type no longer offers it.
Operators: `= Equals`, `!= Not equal`, `> Greater than`, `< Less than`,
`* Any value`. Eq/Neq do case-insensitive string compare against the
canonical property string (so "ByLayer" matches "bylayer"); Gt/Lt parse
both sides as `f64` and reject anything non-numeric; Any skips the
value test entirely. The Value text field is disabled when no Property
is picked or the operator is Any. Append behaves the same as before.
The new value reader is hand-rolled for the four common properties
(layer name, color formatted as `ByLayer`/`ByBlock`/`<aci>`/`r,g,b`,
linetype name, lineweight formatted as `0.30mm`) and falls back through
`geometry_properties()` for type-specific fields — extracting the
property's value out of the `PropValue` variant so EditText / Choice /
ColorChoice all canonicalise the same way the panel will display them.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 21:09:56 +03:00
|
|
|
if s == QSELECT_ANY_TYPE {
|
2026-05-29 20:30:56 +03:00
|
|
|
Message::QSelectSetType(None)
|
|
|
|
|
} else {
|
|
|
|
|
Message::QSelectSetType(Some(s))
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.width(Fill),
|
|
|
|
|
]
|
|
|
|
|
.align_y(iced::Alignment::Center)
|
|
|
|
|
.spacing(8),
|
|
|
|
|
Space::new().height(6),
|
|
|
|
|
row![
|
feat(qselect): per-property filter with operator + value (#28)
Quick Select used to filter only on Object Type and Layer. The classic
QSELECT model — pick a type, pick one of its properties, compare with a
typed value via a logical operator — is much more general: a Line entity
can now be filtered on Start X, Length, Angle, Delta Y, …, and any
entity on the common Layer / Color / Linetype / Lineweight.
Panel layout: Object type → Property → Operator → Value → Append. The
Property dropdown is type-aware: it always lists the four common
properties; picking a specific Object type appends that type's
`geometry_properties` rows, sampled from the first entity of that type
in the active layout (so `Start X`, `Radius`, `Pattern Angle`, … only
appear when their entity type is selected). Changing the Object type
drops the Property when the new type no longer offers it.
Operators: `= Equals`, `!= Not equal`, `> Greater than`, `< Less than`,
`* Any value`. Eq/Neq do case-insensitive string compare against the
canonical property string (so "ByLayer" matches "bylayer"); Gt/Lt parse
both sides as `f64` and reject anything non-numeric; Any skips the
value test entirely. The Value text field is disabled when no Property
is picked or the operator is Any. Append behaves the same as before.
The new value reader is hand-rolled for the four common properties
(layer name, color formatted as `ByLayer`/`ByBlock`/`<aci>`/`r,g,b`,
linetype name, lineweight formatted as `0.30mm`) and falls back through
`geometry_properties()` for type-specific fields — extracting the
property's value out of the `PropValue` variant so EditText / Choice /
ColorChoice all canonicalise the same way the panel will display them.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 21:09:56 +03:00
|
|
|
label("Property:"),
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
pick_list(
|
|
|
|
|
prop_options,
|
|
|
|
|
Some(prop_sel),
|
|
|
|
|
|p: crate::app::QSelectPropertyChoice| {
|
|
|
|
|
if p.field.is_empty() {
|
|
|
|
|
Message::QSelectSetProperty(None)
|
|
|
|
|
} else {
|
|
|
|
|
Message::QSelectSetProperty(Some(p))
|
|
|
|
|
}
|
2026-05-29 20:30:56 +03:00
|
|
|
}
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
)
|
2026-05-29 20:30:56 +03:00
|
|
|
.width(Fill),
|
|
|
|
|
]
|
|
|
|
|
.align_y(iced::Alignment::Center)
|
|
|
|
|
.spacing(8),
|
feat(qselect): per-property filter with operator + value (#28)
Quick Select used to filter only on Object Type and Layer. The classic
QSELECT model — pick a type, pick one of its properties, compare with a
typed value via a logical operator — is much more general: a Line entity
can now be filtered on Start X, Length, Angle, Delta Y, …, and any
entity on the common Layer / Color / Linetype / Lineweight.
Panel layout: Object type → Property → Operator → Value → Append. The
Property dropdown is type-aware: it always lists the four common
properties; picking a specific Object type appends that type's
`geometry_properties` rows, sampled from the first entity of that type
in the active layout (so `Start X`, `Radius`, `Pattern Angle`, … only
appear when their entity type is selected). Changing the Object type
drops the Property when the new type no longer offers it.
Operators: `= Equals`, `!= Not equal`, `> Greater than`, `< Less than`,
`* Any value`. Eq/Neq do case-insensitive string compare against the
canonical property string (so "ByLayer" matches "bylayer"); Gt/Lt parse
both sides as `f64` and reject anything non-numeric; Any skips the
value test entirely. The Value text field is disabled when no Property
is picked or the operator is Any. Append behaves the same as before.
The new value reader is hand-rolled for the four common properties
(layer name, color formatted as `ByLayer`/`ByBlock`/`<aci>`/`r,g,b`,
linetype name, lineweight formatted as `0.30mm`) and falls back through
`geometry_properties()` for type-specific fields — extracting the
property's value out of the `PropValue` variant so EditText / Choice /
ColorChoice all canonicalise the same way the panel will display them.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 21:09:56 +03:00
|
|
|
Space::new().height(6),
|
|
|
|
|
row![
|
|
|
|
|
label("Operator:"),
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
pick_list(
|
|
|
|
|
op_options,
|
|
|
|
|
Some(state.operator),
|
|
|
|
|
Message::QSelectSetOperator
|
|
|
|
|
)
|
|
|
|
|
.width(Fill),
|
feat(qselect): per-property filter with operator + value (#28)
Quick Select used to filter only on Object Type and Layer. The classic
QSELECT model — pick a type, pick one of its properties, compare with a
typed value via a logical operator — is much more general: a Line entity
can now be filtered on Start X, Length, Angle, Delta Y, …, and any
entity on the common Layer / Color / Linetype / Lineweight.
Panel layout: Object type → Property → Operator → Value → Append. The
Property dropdown is type-aware: it always lists the four common
properties; picking a specific Object type appends that type's
`geometry_properties` rows, sampled from the first entity of that type
in the active layout (so `Start X`, `Radius`, `Pattern Angle`, … only
appear when their entity type is selected). Changing the Object type
drops the Property when the new type no longer offers it.
Operators: `= Equals`, `!= Not equal`, `> Greater than`, `< Less than`,
`* Any value`. Eq/Neq do case-insensitive string compare against the
canonical property string (so "ByLayer" matches "bylayer"); Gt/Lt parse
both sides as `f64` and reject anything non-numeric; Any skips the
value test entirely. The Value text field is disabled when no Property
is picked or the operator is Any. Append behaves the same as before.
The new value reader is hand-rolled for the four common properties
(layer name, color formatted as `ByLayer`/`ByBlock`/`<aci>`/`r,g,b`,
linetype name, lineweight formatted as `0.30mm`) and falls back through
`geometry_properties()` for type-specific fields — extracting the
property's value out of the `PropValue` variant so EditText / Choice /
ColorChoice all canonicalise the same way the panel will display them.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 21:09:56 +03:00
|
|
|
]
|
|
|
|
|
.align_y(iced::Alignment::Center)
|
|
|
|
|
.spacing(8),
|
|
|
|
|
Space::new().height(6),
|
|
|
|
|
row![label("Value:"), value_input,]
|
|
|
|
|
.align_y(iced::Alignment::Center)
|
|
|
|
|
.spacing(8),
|
2026-05-29 20:30:56 +03:00
|
|
|
Space::new().height(10),
|
|
|
|
|
row![
|
|
|
|
|
checkbox(state.append)
|
|
|
|
|
.on_toggle(Message::QSelectSetAppend)
|
|
|
|
|
.size(14),
|
|
|
|
|
Space::new().width(6),
|
|
|
|
|
text("Append to current selection").size(12).color(TEXT),
|
|
|
|
|
]
|
|
|
|
|
.align_y(iced::Alignment::Center),
|
|
|
|
|
Space::new().height(14),
|
|
|
|
|
row![
|
|
|
|
|
Space::new().width(Fill),
|
|
|
|
|
btn("Cancel", Message::QSelectClose, BTN_BG, BTN_HOV),
|
|
|
|
|
Space::new().width(8),
|
|
|
|
|
btn("Apply", Message::QSelectApply, BTN_OK, BTN_OK_HOV),
|
|
|
|
|
]
|
|
|
|
|
.align_y(iced::Alignment::Center),
|
|
|
|
|
]
|
|
|
|
|
.spacing(0);
|
|
|
|
|
|
|
|
|
|
let panel = container(panel_body)
|
|
|
|
|
.padding(16)
|
feat(qselect): per-property filter with operator + value (#28)
Quick Select used to filter only on Object Type and Layer. The classic
QSELECT model — pick a type, pick one of its properties, compare with a
typed value via a logical operator — is much more general: a Line entity
can now be filtered on Start X, Length, Angle, Delta Y, …, and any
entity on the common Layer / Color / Linetype / Lineweight.
Panel layout: Object type → Property → Operator → Value → Append. The
Property dropdown is type-aware: it always lists the four common
properties; picking a specific Object type appends that type's
`geometry_properties` rows, sampled from the first entity of that type
in the active layout (so `Start X`, `Radius`, `Pattern Angle`, … only
appear when their entity type is selected). Changing the Object type
drops the Property when the new type no longer offers it.
Operators: `= Equals`, `!= Not equal`, `> Greater than`, `< Less than`,
`* Any value`. Eq/Neq do case-insensitive string compare against the
canonical property string (so "ByLayer" matches "bylayer"); Gt/Lt parse
both sides as `f64` and reject anything non-numeric; Any skips the
value test entirely. The Value text field is disabled when no Property
is picked or the operator is Any. Append behaves the same as before.
The new value reader is hand-rolled for the four common properties
(layer name, color formatted as `ByLayer`/`ByBlock`/`<aci>`/`r,g,b`,
linetype name, lineweight formatted as `0.30mm`) and falls back through
`geometry_properties()` for type-specific fields — extracting the
property's value out of the `PropValue` variant so EditText / Choice /
ColorChoice all canonicalise the same way the panel will display them.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 21:09:56 +03:00
|
|
|
.width(iced::Length::Fixed(400.0))
|
2026-05-29 20:30:56 +03:00
|
|
|
.style(|_: &Theme| container::Style {
|
|
|
|
|
background: Some(Background::Color(BG)),
|
|
|
|
|
border: Border {
|
|
|
|
|
color: BORDER,
|
|
|
|
|
width: 1.0,
|
|
|
|
|
radius: 6.0.into(),
|
|
|
|
|
},
|
|
|
|
|
..Default::default()
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Outside-click catcher — fills the whole screen, sits below the
|
|
|
|
|
// panel. The panel itself is rendered above and absorbs its own
|
|
|
|
|
// clicks via standard widget event handling.
|
|
|
|
|
let catcher = mouse_area(
|
|
|
|
|
container(iced::widget::Space::new().width(Fill).height(Fill))
|
|
|
|
|
.width(Fill)
|
|
|
|
|
.height(Fill),
|
|
|
|
|
)
|
|
|
|
|
.on_press(Message::QSelectClose)
|
|
|
|
|
.on_right_press(Message::QSelectClose);
|
|
|
|
|
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
let centered = container(iced::widget::opaque(panel)).center(Fill);
|
2026-05-29 20:30:56 +03:00
|
|
|
|
|
|
|
|
stack![catcher, centered].into()
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-01 11:31:11 +03:00
|
|
|
/// Content for the floating "Unsaved Changes" OS window.
|
2026-05-01 16:12:32 +03:00
|
|
|
const SAVE_FORMAT_OPTIONS: &[&str] = &[
|
2026-05-12 10:36:15 +03:00
|
|
|
"DWG 2018", "DWG 2013", "DWG 2010", "DWG 2007", "DWG 2004", "DWG 2000", "DWG R14", "DXF 2018",
|
|
|
|
|
"DXF 2013", "DXF 2010", "DXF 2007", "DXF 2004", "DXF 2000", "DXF R14",
|
2026-05-01 16:12:32 +03:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
fn save_as_dialog_window<'a>(
|
|
|
|
|
filename: &'a str,
|
2026-05-01 16:20:38 +03:00
|
|
|
folder: &'a std::path::Path,
|
|
|
|
|
entries: &'a [(String, bool, std::path::PathBuf)],
|
2026-05-01 16:12:32 +03:00
|
|
|
format: &'a str,
|
|
|
|
|
) -> Element<'a, Message> {
|
2026-05-12 10:36:15 +03:00
|
|
|
const BG: Color = Color {
|
|
|
|
|
r: 0.15,
|
|
|
|
|
g: 0.15,
|
|
|
|
|
b: 0.17,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
|
|
|
|
const LIST_BG: Color = Color {
|
|
|
|
|
r: 0.11,
|
|
|
|
|
g: 0.11,
|
|
|
|
|
b: 0.13,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
|
|
|
|
const BORDER: Color = Color {
|
|
|
|
|
r: 0.32,
|
|
|
|
|
g: 0.32,
|
|
|
|
|
b: 0.36,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
|
|
|
|
const TEXT: Color = Color {
|
|
|
|
|
r: 0.90,
|
|
|
|
|
g: 0.90,
|
|
|
|
|
b: 0.90,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
|
|
|
|
const DIM: Color = Color {
|
|
|
|
|
r: 0.58,
|
|
|
|
|
g: 0.58,
|
|
|
|
|
b: 0.62,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
|
|
|
|
const INPUT_BG: Color = Color {
|
|
|
|
|
r: 0.10,
|
|
|
|
|
g: 0.10,
|
|
|
|
|
b: 0.12,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
|
|
|
|
const BTN_OK: Color = Color {
|
|
|
|
|
r: 0.20,
|
|
|
|
|
g: 0.46,
|
|
|
|
|
b: 0.80,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
|
|
|
|
const BTN_HOV: Color = Color {
|
|
|
|
|
r: 0.26,
|
|
|
|
|
g: 0.55,
|
|
|
|
|
b: 0.92,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
|
|
|
|
const BTN_GREY: Color = Color {
|
|
|
|
|
r: 0.26,
|
|
|
|
|
g: 0.26,
|
|
|
|
|
b: 0.29,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
|
|
|
|
const BTN_GHOV: Color = Color {
|
|
|
|
|
r: 0.34,
|
|
|
|
|
g: 0.34,
|
|
|
|
|
b: 0.38,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
|
|
|
|
const DIR_COL: Color = Color {
|
|
|
|
|
r: 0.75,
|
|
|
|
|
g: 0.85,
|
|
|
|
|
b: 1.00,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
|
|
|
|
const FILE_COL: Color = TEXT;
|
|
|
|
|
const ROW_HOV: Color = Color {
|
|
|
|
|
r: 0.22,
|
|
|
|
|
g: 0.24,
|
|
|
|
|
b: 0.28,
|
|
|
|
|
a: 1.0,
|
2026-05-01 16:12:32 +03:00
|
|
|
};
|
|
|
|
|
|
2026-05-12 10:36:15 +03:00
|
|
|
let input_sty =
|
|
|
|
|
|_: &Theme, _: iced::widget::text_input::Status| iced::widget::text_input::Style {
|
|
|
|
|
background: Background::Color(INPUT_BG),
|
|
|
|
|
border: Border {
|
|
|
|
|
color: BORDER,
|
|
|
|
|
width: 1.0,
|
|
|
|
|
radius: 4.0.into(),
|
|
|
|
|
},
|
|
|
|
|
icon: TEXT,
|
|
|
|
|
placeholder: DIM,
|
|
|
|
|
value: TEXT,
|
|
|
|
|
selection: Color {
|
|
|
|
|
r: 0.20,
|
|
|
|
|
g: 0.46,
|
|
|
|
|
b: 0.80,
|
|
|
|
|
a: 0.45,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-01 16:20:38 +03:00
|
|
|
let btn = |lbl: &'static str, msg: Message, base: Color, hov: Color| {
|
|
|
|
|
button(text(lbl).size(12).color(TEXT))
|
2026-05-01 16:12:32 +03:00
|
|
|
.on_press(msg)
|
2026-05-01 16:20:38 +03:00
|
|
|
.style(move |_: &Theme, st| button::Style {
|
|
|
|
|
background: Some(Background::Color(
|
2026-05-12 10:36:15 +03:00
|
|
|
if matches!(st, button::Status::Hovered | button::Status::Pressed) {
|
|
|
|
|
hov
|
|
|
|
|
} else {
|
|
|
|
|
base
|
|
|
|
|
},
|
2026-05-01 16:20:38 +03:00
|
|
|
)),
|
|
|
|
|
text_color: TEXT,
|
2026-05-12 10:36:15 +03:00
|
|
|
border: Border {
|
|
|
|
|
color: BORDER,
|
|
|
|
|
width: 1.0,
|
|
|
|
|
radius: 4.0.into(),
|
|
|
|
|
},
|
2026-05-01 16:20:38 +03:00
|
|
|
..Default::default()
|
2026-05-01 16:12:32 +03:00
|
|
|
})
|
2026-05-01 16:20:38 +03:00
|
|
|
.padding([4, 12])
|
2026-05-01 16:12:32 +03:00
|
|
|
};
|
|
|
|
|
|
2026-05-01 16:20:38 +03:00
|
|
|
// ── Path bar ─────────────────────────────────────────────────────────
|
|
|
|
|
let path_str = folder.to_string_lossy().into_owned();
|
2026-05-12 10:36:15 +03:00
|
|
|
let up_path = folder.parent().map(|p| p.to_path_buf());
|
2026-05-01 16:20:38 +03:00
|
|
|
let path_bar = row![
|
|
|
|
|
{
|
|
|
|
|
let up_msg = up_path.map(Message::SaveDialogNavigate);
|
2026-06-18 12:29:32 +03:00
|
|
|
let b = button(crate::ui::icons::tinted(crate::ui::icons::UP, 14.0, TEXT))
|
2026-05-01 16:20:38 +03:00
|
|
|
.style(|_: &Theme, st| button::Style {
|
|
|
|
|
background: Some(Background::Color(
|
2026-05-12 10:36:15 +03:00
|
|
|
if matches!(st, button::Status::Hovered | button::Status::Pressed) {
|
|
|
|
|
BTN_GHOV
|
|
|
|
|
} else {
|
|
|
|
|
BTN_GREY
|
|
|
|
|
},
|
2026-05-01 16:20:38 +03:00
|
|
|
)),
|
|
|
|
|
text_color: TEXT,
|
2026-05-12 10:36:15 +03:00
|
|
|
border: Border {
|
|
|
|
|
color: BORDER,
|
|
|
|
|
width: 1.0,
|
|
|
|
|
radius: 4.0.into(),
|
|
|
|
|
},
|
2026-05-01 16:20:38 +03:00
|
|
|
..Default::default()
|
|
|
|
|
})
|
|
|
|
|
.padding([3, 10]);
|
2026-05-12 10:36:15 +03:00
|
|
|
if let Some(msg) = up_msg {
|
|
|
|
|
b.on_press(msg)
|
|
|
|
|
} else {
|
|
|
|
|
b
|
|
|
|
|
}
|
2026-05-01 16:20:38 +03:00
|
|
|
},
|
|
|
|
|
Space::new().width(8),
|
2026-05-12 10:36:15 +03:00
|
|
|
container(text(path_str.clone()).size(12).color(DIM))
|
|
|
|
|
.style(|_: &Theme| container::Style {
|
|
|
|
|
background: Some(Background::Color(INPUT_BG)),
|
|
|
|
|
border: Border {
|
|
|
|
|
color: BORDER,
|
|
|
|
|
width: 1.0,
|
|
|
|
|
radius: 4.0.into()
|
|
|
|
|
},
|
|
|
|
|
..Default::default()
|
|
|
|
|
})
|
|
|
|
|
.padding([4, 8])
|
|
|
|
|
.width(Fill),
|
2026-05-01 16:20:38 +03:00
|
|
|
]
|
|
|
|
|
.align_y(iced::Alignment::Center);
|
|
|
|
|
|
|
|
|
|
// ── File list ─────────────────────────────────────────────────────────
|
|
|
|
|
let file_list: Element<'_, Message> = {
|
2026-05-12 10:36:15 +03:00
|
|
|
let rows: Vec<Element<'_, Message>> = entries
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|(name, is_dir, path)| {
|
2026-06-18 12:29:32 +03:00
|
|
|
let icon_bytes = if *is_dir {
|
|
|
|
|
crate::ui::icons::FOLDER
|
|
|
|
|
} else {
|
|
|
|
|
crate::ui::icons::DOC
|
|
|
|
|
};
|
2026-05-12 10:36:15 +03:00
|
|
|
let color = if *is_dir { DIR_COL } else { FILE_COL };
|
|
|
|
|
let p = path.clone();
|
|
|
|
|
let d = *is_dir;
|
|
|
|
|
mouse_area(
|
|
|
|
|
container(
|
|
|
|
|
row![
|
2026-06-18 12:29:32 +03:00
|
|
|
crate::ui::icons::tinted(icon_bytes, 13.0, color),
|
2026-05-12 10:36:15 +03:00
|
|
|
Space::new().width(6),
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
text(crate::ui::text_util::elide(name.as_str(), 48))
|
|
|
|
|
.size(13)
|
|
|
|
|
.color(color),
|
2026-05-12 10:36:15 +03:00
|
|
|
]
|
|
|
|
|
.align_y(iced::Alignment::Center),
|
|
|
|
|
)
|
|
|
|
|
.style(|_: &Theme| container::Style {
|
|
|
|
|
..Default::default()
|
|
|
|
|
})
|
|
|
|
|
.padding([3, 8])
|
|
|
|
|
.width(Fill),
|
2026-05-01 16:20:38 +03:00
|
|
|
)
|
2026-05-12 10:36:15 +03:00
|
|
|
.on_press(Message::SaveDialogEntryClicked(p, d))
|
|
|
|
|
.into()
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
2026-05-01 16:12:32 +03:00
|
|
|
|
2026-05-12 10:36:15 +03:00
|
|
|
container(iced::widget::scrollable(
|
|
|
|
|
column(rows).spacing(1).width(Fill),
|
|
|
|
|
))
|
2026-05-01 16:20:38 +03:00
|
|
|
.style(|_: &Theme| container::Style {
|
|
|
|
|
background: Some(Background::Color(LIST_BG)),
|
2026-05-12 10:36:15 +03:00
|
|
|
border: Border {
|
|
|
|
|
color: BORDER,
|
|
|
|
|
width: 1.0,
|
|
|
|
|
radius: 4.0.into(),
|
|
|
|
|
},
|
2026-05-01 16:20:38 +03:00
|
|
|
..Default::default()
|
|
|
|
|
})
|
|
|
|
|
.width(Fill)
|
|
|
|
|
.height(Fill)
|
|
|
|
|
.into()
|
|
|
|
|
};
|
|
|
|
|
let _ = ROW_HOV; // used conceptually, suppress warning
|
2026-05-01 16:12:32 +03:00
|
|
|
|
2026-05-01 16:20:38 +03:00
|
|
|
let sel_fmt = SAVE_FORMAT_OPTIONS.iter().copied().find(|&s| s == format);
|
2026-05-12 10:36:15 +03:00
|
|
|
let label = |s: &'static str| text(s).size(11).color(DIM);
|
2026-05-01 16:20:38 +03:00
|
|
|
|
|
|
|
|
// ── Bottom controls ───────────────────────────────────────────────────
|
|
|
|
|
let bottom = column![
|
|
|
|
|
row![
|
|
|
|
|
label("File name:").width(90),
|
|
|
|
|
text_input("drawing.dwg", filename)
|
2026-05-01 16:12:32 +03:00
|
|
|
.on_input(Message::SaveDialogFilenameChanged)
|
2026-05-01 16:20:38 +03:00
|
|
|
.style(input_sty)
|
2026-05-01 16:12:32 +03:00
|
|
|
.size(13)
|
2026-05-01 16:20:38 +03:00
|
|
|
.padding([5, 8])
|
|
|
|
|
.width(Fill),
|
|
|
|
|
]
|
|
|
|
|
.align_y(iced::Alignment::Center)
|
|
|
|
|
.spacing(6),
|
|
|
|
|
Space::new().height(6),
|
|
|
|
|
row![
|
|
|
|
|
label("Format:").width(90),
|
2026-05-12 10:36:15 +03:00
|
|
|
pick_list(SAVE_FORMAT_OPTIONS, sel_fmt, |s: &str| {
|
|
|
|
|
Message::SaveDialogFormatChanged(s.to_string())
|
|
|
|
|
})
|
|
|
|
|
.width(Fill),
|
2026-05-01 16:20:38 +03:00
|
|
|
]
|
|
|
|
|
.align_y(iced::Alignment::Center)
|
|
|
|
|
.spacing(6),
|
|
|
|
|
Space::new().height(12),
|
|
|
|
|
row![
|
|
|
|
|
Space::new().width(Fill),
|
2026-05-12 10:36:15 +03:00
|
|
|
btn("Save", Message::SaveDialogConfirm, BTN_OK, BTN_HOV),
|
2026-05-01 16:20:38 +03:00
|
|
|
Space::new().width(8),
|
2026-05-12 10:36:15 +03:00
|
|
|
btn("Cancel", Message::SaveDialogCancel, BTN_GREY, BTN_GHOV),
|
2026-05-01 16:20:38 +03:00
|
|
|
],
|
|
|
|
|
]
|
|
|
|
|
.spacing(0);
|
|
|
|
|
|
|
|
|
|
container(
|
|
|
|
|
column![
|
|
|
|
|
path_bar,
|
|
|
|
|
Space::new().height(8),
|
|
|
|
|
file_list,
|
|
|
|
|
Space::new().height(10),
|
|
|
|
|
bottom,
|
2026-05-01 16:12:32 +03:00
|
|
|
]
|
|
|
|
|
.spacing(0),
|
|
|
|
|
)
|
2026-05-01 16:20:38 +03:00
|
|
|
.style(|_: &Theme| container::Style {
|
2026-05-01 16:12:32 +03:00
|
|
|
background: Some(Background::Color(BG)),
|
|
|
|
|
..Default::default()
|
|
|
|
|
})
|
2026-05-01 16:20:38 +03:00
|
|
|
.padding([14, 16])
|
2026-05-01 16:12:32 +03:00
|
|
|
.width(Fill)
|
|
|
|
|
.height(Fill)
|
|
|
|
|
.into()
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-01 11:31:11 +03:00
|
|
|
fn unsaved_changes_dialog_window(name: &str) -> Element<'static, Message> {
|
2026-05-12 10:36:15 +03:00
|
|
|
const BG: Color = Color {
|
|
|
|
|
r: 0.18,
|
|
|
|
|
g: 0.18,
|
|
|
|
|
b: 0.20,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
|
|
|
|
const BORDER_COL: Color = Color {
|
|
|
|
|
r: 0.38,
|
|
|
|
|
g: 0.38,
|
|
|
|
|
b: 0.42,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
|
|
|
|
const TEXT_COL: Color = Color {
|
|
|
|
|
r: 0.90,
|
|
|
|
|
g: 0.90,
|
|
|
|
|
b: 0.90,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
|
|
|
|
const BTN_SAVE: Color = Color {
|
|
|
|
|
r: 0.20,
|
|
|
|
|
g: 0.46,
|
|
|
|
|
b: 0.80,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
|
|
|
|
const BTN_HOVER: Color = Color {
|
|
|
|
|
r: 0.26,
|
|
|
|
|
g: 0.55,
|
|
|
|
|
b: 0.92,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
|
|
|
|
const BTN_DISC: Color = Color {
|
|
|
|
|
r: 0.28,
|
|
|
|
|
g: 0.28,
|
|
|
|
|
b: 0.30,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
|
|
|
|
const BTN_DHOV: Color = Color {
|
|
|
|
|
r: 0.36,
|
|
|
|
|
g: 0.36,
|
|
|
|
|
b: 0.40,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
2026-05-01 11:31:11 +03:00
|
|
|
|
|
|
|
|
let body_text = format!("Do you want to save changes to \"{}\"?", name);
|
|
|
|
|
|
|
|
|
|
let btn = |label: &'static str, msg: Message, base: Color, hov: Color| {
|
|
|
|
|
button(text(label).size(13).color(TEXT_COL))
|
|
|
|
|
.on_press(msg)
|
|
|
|
|
.style(move |_: &Theme, status| button::Style {
|
|
|
|
|
background: Some(Background::Color(match status {
|
|
|
|
|
button::Status::Hovered | button::Status::Pressed => hov,
|
|
|
|
|
_ => base,
|
|
|
|
|
})),
|
|
|
|
|
text_color: TEXT_COL,
|
2026-05-12 10:36:15 +03:00
|
|
|
border: Border {
|
|
|
|
|
color: BORDER_COL,
|
|
|
|
|
width: 1.0,
|
|
|
|
|
radius: 4.0.into(),
|
|
|
|
|
},
|
2026-05-01 11:31:11 +03:00
|
|
|
shadow: iced::Shadow::default(),
|
|
|
|
|
snap: false,
|
|
|
|
|
})
|
|
|
|
|
.padding([6, 18])
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
container(
|
|
|
|
|
column![
|
|
|
|
|
text(body_text).size(13).color(TEXT_COL),
|
|
|
|
|
iced::widget::Space::new().height(20),
|
|
|
|
|
row![
|
2026-05-12 10:36:15 +03:00
|
|
|
btn("Save", Message::UnsavedDialogSave, BTN_SAVE, BTN_HOVER),
|
2026-05-01 11:31:11 +03:00
|
|
|
iced::widget::Space::new().width(8),
|
|
|
|
|
btn("Discard", Message::UnsavedDialogDiscard, BTN_DISC, BTN_DHOV),
|
|
|
|
|
iced::widget::Space::new().width(8),
|
2026-05-12 10:36:15 +03:00
|
|
|
btn("Cancel", Message::UnsavedDialogCancel, BTN_DISC, BTN_DHOV),
|
2026-05-01 11:31:11 +03:00
|
|
|
],
|
|
|
|
|
]
|
|
|
|
|
.spacing(0),
|
|
|
|
|
)
|
|
|
|
|
.style(move |_: &Theme| container::Style {
|
|
|
|
|
background: Some(Background::Color(BG)),
|
|
|
|
|
..Default::default()
|
|
|
|
|
})
|
|
|
|
|
.center(Fill)
|
|
|
|
|
.padding([24, 28])
|
|
|
|
|
.into()
|
|
|
|
|
}
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
|
2026-06-12 07:50:41 +00:00
|
|
|
/// First-launch prompt offering to register Open CAD Studio as the default
|
|
|
|
|
/// handler for .dwg / .dxf. "Yes" runs the platform association call; "Not now"
|
|
|
|
|
/// just dismisses. Either answer flips the persisted `default_assoc_prompted`
|
|
|
|
|
/// flag so the dialog never reappears.
|
|
|
|
|
fn default_assoc_dialog_window() -> Element<'static, Message> {
|
|
|
|
|
const BG: Color = Color {
|
|
|
|
|
r: 0.18,
|
|
|
|
|
g: 0.18,
|
|
|
|
|
b: 0.20,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
|
|
|
|
const BORDER_COL: Color = Color {
|
|
|
|
|
r: 0.38,
|
|
|
|
|
g: 0.38,
|
|
|
|
|
b: 0.42,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
|
|
|
|
const TEXT_COL: Color = Color {
|
|
|
|
|
r: 0.90,
|
|
|
|
|
g: 0.90,
|
|
|
|
|
b: 0.90,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
|
|
|
|
const DIM_COL: Color = Color {
|
|
|
|
|
r: 0.62,
|
|
|
|
|
g: 0.62,
|
|
|
|
|
b: 0.66,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
|
|
|
|
const BTN_YES: Color = Color {
|
|
|
|
|
r: 0.20,
|
|
|
|
|
g: 0.46,
|
|
|
|
|
b: 0.80,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
|
|
|
|
const BTN_YHOV: Color = Color {
|
|
|
|
|
r: 0.26,
|
|
|
|
|
g: 0.55,
|
|
|
|
|
b: 0.92,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
|
|
|
|
const BTN_NO: Color = Color {
|
|
|
|
|
r: 0.28,
|
|
|
|
|
g: 0.28,
|
|
|
|
|
b: 0.30,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
|
|
|
|
const BTN_NHOV: Color = Color {
|
|
|
|
|
r: 0.36,
|
|
|
|
|
g: 0.36,
|
|
|
|
|
b: 0.40,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let btn = |label: &'static str, msg: Message, base: Color, hov: Color| {
|
|
|
|
|
button(text(label).size(13).color(TEXT_COL))
|
|
|
|
|
.on_press(msg)
|
|
|
|
|
.style(move |_: &Theme, status| button::Style {
|
|
|
|
|
background: Some(Background::Color(match status {
|
|
|
|
|
button::Status::Hovered | button::Status::Pressed => hov,
|
|
|
|
|
_ => base,
|
|
|
|
|
})),
|
|
|
|
|
text_color: TEXT_COL,
|
|
|
|
|
border: Border {
|
|
|
|
|
color: BORDER_COL,
|
|
|
|
|
width: 1.0,
|
|
|
|
|
radius: 4.0.into(),
|
|
|
|
|
},
|
|
|
|
|
shadow: iced::Shadow::default(),
|
|
|
|
|
snap: false,
|
|
|
|
|
})
|
|
|
|
|
.padding([6, 18])
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
container(
|
|
|
|
|
column![
|
|
|
|
|
text("Make Open CAD Studio your default CAD app?")
|
|
|
|
|
.size(15)
|
|
|
|
|
.color(TEXT_COL),
|
|
|
|
|
iced::widget::Space::new().height(10),
|
|
|
|
|
text("Open .dwg and .dxf drawings in Open CAD Studio by default. You can change this later in your system settings.")
|
|
|
|
|
.size(12)
|
|
|
|
|
.color(DIM_COL),
|
|
|
|
|
iced::widget::Space::new().height(22),
|
|
|
|
|
row![
|
|
|
|
|
iced::widget::Space::new().width(Fill),
|
|
|
|
|
btn("Not now", Message::AssocPromptNo, BTN_NO, BTN_NHOV),
|
|
|
|
|
iced::widget::Space::new().width(8),
|
|
|
|
|
btn("Yes, set as default", Message::AssocPromptYes, BTN_YES, BTN_YHOV),
|
|
|
|
|
]
|
|
|
|
|
.align_y(iced::Center),
|
|
|
|
|
]
|
|
|
|
|
.spacing(0),
|
|
|
|
|
)
|
|
|
|
|
.style(move |_: &Theme| container::Style {
|
|
|
|
|
background: Some(Background::Color(BG)),
|
|
|
|
|
..Default::default()
|
|
|
|
|
})
|
|
|
|
|
.center(Fill)
|
|
|
|
|
.padding([24, 28])
|
|
|
|
|
.into()
|
|
|
|
|
}
|
|
|
|
|
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
// ── Start / Welcome page ──────────────────────────────────────────────────
|
|
|
|
|
//
|
|
|
|
|
// Renders in place of the model-space viewport when the active tab is the
|
|
|
|
|
// fixed Start tab (`DocumentTab::is_start`). English-only by design — this
|
|
|
|
|
// is the public welcome screen and stays consistent across locales.
|
|
|
|
|
//
|
|
|
|
|
// The page picks up the application icon's red-brown (#B03020) as a tint so
|
2026-05-23 17:16:15 +03:00
|
|
|
// it visually belongs to OpenCADStudio without overpowering the dark workspace.
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
const BRAND: Color = Color {
|
|
|
|
|
r: 0.690,
|
|
|
|
|
g: 0.188,
|
|
|
|
|
b: 0.125,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
}; // #B03020
|
|
|
|
|
const BRAND_DARK: Color = Color {
|
|
|
|
|
r: 0.45,
|
|
|
|
|
g: 0.12,
|
|
|
|
|
b: 0.08,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
|
|
|
|
|
pub(super) fn start_page_view<'a>() -> Element<'a, Message> {
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
const TEXT: Color = Color {
|
|
|
|
|
r: 0.94,
|
|
|
|
|
g: 0.93,
|
|
|
|
|
b: 0.92,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
|
|
|
|
const MUTED: Color = Color {
|
|
|
|
|
r: 0.62,
|
|
|
|
|
g: 0.62,
|
|
|
|
|
b: 0.62,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
|
|
|
|
const CARD_BG: Color = Color {
|
|
|
|
|
r: 0.12,
|
|
|
|
|
g: 0.12,
|
|
|
|
|
b: 0.13,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
|
|
|
|
const CARD_BORDER: Color = Color {
|
|
|
|
|
r: 0.20,
|
|
|
|
|
g: 0.20,
|
|
|
|
|
b: 0.22,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
|
2026-05-23 17:16:15 +03:00
|
|
|
// Brand-tinted "Welcome to" — the "OpenCADStudio" word takes the accent colour
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
// (Thunderbird-style coloured headline split).
|
|
|
|
|
let headline = row![
|
|
|
|
|
text("Welcome to ").size(40).color(TEXT),
|
2026-05-23 17:16:15 +03:00
|
|
|
text("Open CAD Studio").size(40).color(BRAND),
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
]
|
|
|
|
|
.align_y(iced::Center);
|
|
|
|
|
|
|
|
|
|
let subtitle = text(
|
2026-05-23 17:16:15 +03:00
|
|
|
"Open CAD Studio is an open-source CAD viewer and editor — a gift from contributors like you. \
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
Open a DWG/DXF file, start a new drawing, or help shape what comes next.",
|
|
|
|
|
)
|
|
|
|
|
.size(13)
|
|
|
|
|
.color(MUTED);
|
|
|
|
|
|
|
|
|
|
// Plain outlined button (Open / New / Help / Contribute).
|
|
|
|
|
let outline_btn = |label: &'static str, msg: Message| {
|
|
|
|
|
button(text(label).size(14).color(TEXT))
|
|
|
|
|
.on_press(msg)
|
|
|
|
|
.padding([10, 22])
|
|
|
|
|
.style(move |_: &Theme, status| button::Style {
|
|
|
|
|
background: Some(Background::Color(match status {
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
button::Status::Hovered => Color {
|
|
|
|
|
r: 0.18,
|
|
|
|
|
g: 0.18,
|
|
|
|
|
b: 0.20,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
},
|
|
|
|
|
_ => Color {
|
|
|
|
|
r: 0.13,
|
|
|
|
|
g: 0.13,
|
|
|
|
|
b: 0.15,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
},
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
})),
|
|
|
|
|
text_color: TEXT,
|
|
|
|
|
border: Border {
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
color: Color {
|
|
|
|
|
r: 0.30,
|
|
|
|
|
g: 0.30,
|
|
|
|
|
b: 0.33,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
},
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
width: 1.0,
|
|
|
|
|
radius: 6.0.into(),
|
|
|
|
|
},
|
|
|
|
|
..Default::default()
|
|
|
|
|
})
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Donate — the prominent call-to-action. Solid brand fill, white text.
|
|
|
|
|
let donate_btn = {
|
|
|
|
|
button(
|
|
|
|
|
row![
|
2026-06-18 12:29:32 +03:00
|
|
|
crate::ui::icons::tinted(crate::ui::icons::HEART, 14.0, Color::WHITE),
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
text("Donate").size(14).color(Color::WHITE),
|
|
|
|
|
]
|
2026-06-18 12:29:32 +03:00
|
|
|
.spacing(5)
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
.align_y(iced::Center),
|
|
|
|
|
)
|
|
|
|
|
.on_press(Message::RibbonToolClick {
|
|
|
|
|
tool_id: "DONATE".to_string(),
|
|
|
|
|
event: crate::modules::ModuleEvent::Command("DONATE".to_string()),
|
|
|
|
|
})
|
|
|
|
|
.padding([12, 28])
|
|
|
|
|
.style(|_: &Theme, status| button::Style {
|
|
|
|
|
background: Some(Background::Color(match status {
|
|
|
|
|
button::Status::Hovered => BRAND_DARK,
|
|
|
|
|
_ => BRAND,
|
|
|
|
|
})),
|
|
|
|
|
text_color: Color::WHITE,
|
|
|
|
|
border: Border {
|
|
|
|
|
color: BRAND_DARK,
|
|
|
|
|
width: 1.0,
|
|
|
|
|
radius: 6.0.into(),
|
|
|
|
|
},
|
|
|
|
|
shadow: iced::Shadow {
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
color: Color {
|
|
|
|
|
r: 0.0,
|
|
|
|
|
g: 0.0,
|
|
|
|
|
b: 0.0,
|
|
|
|
|
a: 0.4,
|
|
|
|
|
},
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
offset: iced::Vector::new(0.0, 2.0),
|
|
|
|
|
blur_radius: 6.0,
|
|
|
|
|
},
|
|
|
|
|
..Default::default()
|
|
|
|
|
})
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let primary_row = row![
|
|
|
|
|
outline_btn("New Drawing", Message::TabNew),
|
|
|
|
|
outline_btn("Open File…", Message::OpenFile),
|
|
|
|
|
donate_btn,
|
|
|
|
|
]
|
|
|
|
|
.spacing(12)
|
|
|
|
|
.align_y(iced::Center);
|
|
|
|
|
|
2026-06-22 02:03:34 +03:00
|
|
|
#[cfg_attr(target_arch = "wasm32", allow(unused_mut))]
|
2026-06-17 01:33:54 +03:00
|
|
|
let mut secondary_row = row![
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
outline_btn(
|
2026-06-15 01:12:29 +03:00
|
|
|
"Send Feedback",
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
Message::RibbonToolClick {
|
|
|
|
|
tool_id: "REPORT".to_string(),
|
|
|
|
|
event: crate::modules::ModuleEvent::Command("REPORT".to_string()),
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
outline_btn(
|
|
|
|
|
"Release Notes",
|
|
|
|
|
Message::RibbonToolClick {
|
|
|
|
|
tool_id: "CHANGELOG".to_string(),
|
|
|
|
|
event: crate::modules::ModuleEvent::Command("CHANGELOG".to_string()),
|
|
|
|
|
},
|
|
|
|
|
),
|
2026-06-15 01:46:09 +03:00
|
|
|
outline_btn("Plugins", Message::PluginManagerOpen),
|
2026-06-17 01:33:54 +03:00
|
|
|
];
|
|
|
|
|
// The web build is already in the browser, so only the desktop offers a
|
|
|
|
|
// link to the web version.
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
{
|
|
|
|
|
// Bright ribbon blue (matches the active-tool accent), filled.
|
|
|
|
|
secondary_row = secondary_row.push(
|
|
|
|
|
button(text("OCS Web").size(14).color(Color::WHITE))
|
|
|
|
|
.on_press(Message::RibbonToolClick {
|
|
|
|
|
tool_id: "WEBVERSION".to_string(),
|
|
|
|
|
event: crate::modules::ModuleEvent::Command("WEBVERSION".to_string()),
|
|
|
|
|
})
|
|
|
|
|
.padding([10, 22])
|
|
|
|
|
.style(|_: &Theme, status| button::Style {
|
|
|
|
|
background: Some(Background::Color(match status {
|
|
|
|
|
button::Status::Hovered => Color {
|
|
|
|
|
r: 0.15,
|
|
|
|
|
g: 0.45,
|
|
|
|
|
b: 0.78,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
},
|
|
|
|
|
_ => Color {
|
|
|
|
|
r: 0.20,
|
|
|
|
|
g: 0.55,
|
|
|
|
|
b: 0.90,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
},
|
|
|
|
|
})),
|
|
|
|
|
text_color: Color::WHITE,
|
|
|
|
|
border: Border {
|
|
|
|
|
color: Color {
|
|
|
|
|
r: 0.20,
|
|
|
|
|
g: 0.55,
|
|
|
|
|
b: 0.90,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
},
|
|
|
|
|
width: 1.0,
|
|
|
|
|
radius: 6.0.into(),
|
|
|
|
|
},
|
|
|
|
|
..Default::default()
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
let secondary_row = secondary_row.spacing(12).align_y(iced::Center);
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
|
|
|
|
|
let card_section = |heading: &'static str, sub: &'static str, body: &'static str| {
|
|
|
|
|
container(
|
|
|
|
|
column![
|
|
|
|
|
text(heading).size(20).color(TEXT),
|
|
|
|
|
text(sub).size(13).color(MUTED),
|
|
|
|
|
Space::new().height(iced::Length::Fixed(10.0)),
|
|
|
|
|
text(body).size(12).color(MUTED),
|
|
|
|
|
]
|
|
|
|
|
.spacing(6),
|
|
|
|
|
)
|
|
|
|
|
.padding(20)
|
|
|
|
|
.width(Fill)
|
|
|
|
|
.style(|_: &Theme| container::Style {
|
|
|
|
|
background: Some(Background::Color(CARD_BG)),
|
|
|
|
|
border: Border {
|
|
|
|
|
color: CARD_BORDER,
|
|
|
|
|
width: 1.0,
|
|
|
|
|
radius: 8.0.into(),
|
|
|
|
|
},
|
|
|
|
|
..Default::default()
|
|
|
|
|
})
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let cards = row![
|
|
|
|
|
card_section(
|
|
|
|
|
"Free and open source",
|
2026-05-23 17:16:15 +03:00
|
|
|
"Open CAD Studio belongs to its users.",
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
"Source is openly developed and distributed under a permissive licence. \
|
2026-05-23 17:16:15 +03:00
|
|
|
You are free to use, study, modify, and share Open CAD Studio.",
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
),
|
|
|
|
|
card_section(
|
|
|
|
|
"Community driven",
|
|
|
|
|
"Be part of the story.",
|
|
|
|
|
"Anyone can contribute — translations, bug reports, feature ideas, code, \
|
|
|
|
|
or simply spreading the word. Your involvement shapes the project.",
|
|
|
|
|
),
|
|
|
|
|
]
|
|
|
|
|
.spacing(16);
|
|
|
|
|
|
|
|
|
|
// Buttons sit on a transparent container with a large, brand-tinted
|
|
|
|
|
// ambient shadow (offset = 0, big blur) — produces a soft halo behind
|
|
|
|
|
// the action row, matching the Thunderbird coloured-glow look against
|
|
|
|
|
// the dark page.
|
|
|
|
|
let primary_glow = container(primary_row)
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
.padding(iced::Padding {
|
|
|
|
|
top: 4.0,
|
|
|
|
|
right: 8.0,
|
|
|
|
|
bottom: 4.0,
|
|
|
|
|
left: 8.0,
|
|
|
|
|
})
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
.style(|_: &Theme| container::Style {
|
|
|
|
|
background: Some(Background::Color(Color::TRANSPARENT)),
|
|
|
|
|
shadow: iced::Shadow {
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
color: Color {
|
|
|
|
|
r: BRAND.r,
|
|
|
|
|
g: BRAND.g,
|
|
|
|
|
b: BRAND.b,
|
|
|
|
|
a: 0.45,
|
|
|
|
|
},
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
offset: iced::Vector::ZERO,
|
|
|
|
|
blur_radius: 80.0,
|
|
|
|
|
},
|
|
|
|
|
..Default::default()
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let content = column![
|
|
|
|
|
Space::new().height(iced::Length::Fixed(28.0)),
|
|
|
|
|
container(headline).center_x(Fill),
|
|
|
|
|
container(subtitle).center_x(Fill).padding([10, 60]),
|
|
|
|
|
Space::new().height(iced::Length::Fixed(22.0)),
|
|
|
|
|
container(primary_glow).center_x(Fill),
|
|
|
|
|
Space::new().height(iced::Length::Fixed(10.0)),
|
|
|
|
|
container(secondary_row).center_x(Fill),
|
|
|
|
|
Space::new().height(iced::Length::Fixed(40.0)),
|
|
|
|
|
cards,
|
|
|
|
|
Space::new().height(Fill),
|
|
|
|
|
]
|
|
|
|
|
.spacing(0)
|
|
|
|
|
.width(Fill)
|
|
|
|
|
.height(Fill);
|
|
|
|
|
|
|
|
|
|
// Page background reverts to plain dark — the glow alone provides the
|
|
|
|
|
// brand colour cue, the rest of the page stays neutral so it reads as
|
|
|
|
|
// "workspace area" not "advertising banner".
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
const PAGE_BG: Color = Color {
|
|
|
|
|
r: 0.08,
|
|
|
|
|
g: 0.08,
|
|
|
|
|
b: 0.085,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
container(content)
|
|
|
|
|
.style(|_: &Theme| container::Style {
|
|
|
|
|
background: Some(Background::Color(PAGE_BG)),
|
|
|
|
|
..Default::default()
|
|
|
|
|
})
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
.padding(iced::Padding {
|
|
|
|
|
top: 40.0,
|
|
|
|
|
right: 60.0,
|
|
|
|
|
bottom: 40.0,
|
|
|
|
|
left: 60.0,
|
|
|
|
|
})
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
.width(Fill)
|
|
|
|
|
.height(Fill)
|
|
|
|
|
.into()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Recent Documents panel (Start tab left rail) ──────────────────────────
|
|
|
|
|
//
|
|
|
|
|
// Slots into the same `row![properties_el, viewport_stack]` position the
|
|
|
|
|
// Properties panel normally occupies, but only when the active tab is the
|
|
|
|
|
// Start tab. The list is restored from disk at boot and re-saved on every
|
|
|
|
|
// open — entries persist across sessions.
|
|
|
|
|
pub(super) fn recent_files_panel<'a>(recents: &'a [std::path::PathBuf]) -> Element<'a, Message> {
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
const PANEL_BG: Color = Color {
|
|
|
|
|
r: 0.10,
|
|
|
|
|
g: 0.10,
|
|
|
|
|
b: 0.11,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
|
|
|
|
const PANEL_BORDER: Color = Color {
|
|
|
|
|
r: 0.18,
|
|
|
|
|
g: 0.18,
|
|
|
|
|
b: 0.20,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
|
|
|
|
const ITEM_HOVER: Color = Color {
|
|
|
|
|
r: 0.16,
|
|
|
|
|
g: 0.16,
|
|
|
|
|
b: 0.18,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
|
|
|
|
const TEXT: Color = Color {
|
|
|
|
|
r: 0.92,
|
|
|
|
|
g: 0.91,
|
|
|
|
|
b: 0.90,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
|
|
|
|
const MUTED: Color = Color {
|
|
|
|
|
r: 0.60,
|
|
|
|
|
g: 0.60,
|
|
|
|
|
b: 0.62,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
};
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
let header = container(text("Recent Documents").size(11).color(MUTED)).padding(iced::Padding {
|
|
|
|
|
top: 12.0,
|
|
|
|
|
right: 14.0,
|
|
|
|
|
bottom: 8.0,
|
|
|
|
|
left: 14.0,
|
|
|
|
|
});
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
|
|
|
|
|
let body: Element<'a, Message> = if recents.is_empty() {
|
|
|
|
|
container(
|
|
|
|
|
text("Files you open will show up here.")
|
|
|
|
|
.size(11)
|
|
|
|
|
.color(MUTED),
|
|
|
|
|
)
|
|
|
|
|
.padding([10, 14])
|
|
|
|
|
.into()
|
|
|
|
|
} else {
|
|
|
|
|
let mut col = column![].spacing(0);
|
|
|
|
|
for path in recents {
|
|
|
|
|
let name = path
|
|
|
|
|
.file_name()
|
|
|
|
|
.map(|n| n.to_string_lossy().into_owned())
|
|
|
|
|
.unwrap_or_else(|| path.to_string_lossy().into_owned());
|
|
|
|
|
let dir = path
|
|
|
|
|
.parent()
|
|
|
|
|
.map(|p| p.to_string_lossy().into_owned())
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
|
|
|
|
|
let path_for_open = path.clone();
|
|
|
|
|
let open_btn = button(
|
|
|
|
|
column![
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
text(crate::ui::text_util::elide(&name, 32))
|
|
|
|
|
.size(12)
|
|
|
|
|
.color(TEXT),
|
|
|
|
|
text(crate::ui::text_util::elide(&dir, 42))
|
|
|
|
|
.size(10)
|
|
|
|
|
.color(MUTED),
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
]
|
|
|
|
|
.spacing(2),
|
|
|
|
|
)
|
|
|
|
|
.on_press(Message::OpenRecent(path_for_open))
|
|
|
|
|
.padding([6, 12])
|
|
|
|
|
.width(Fill)
|
|
|
|
|
.style(move |_: &Theme, status| button::Style {
|
|
|
|
|
background: Some(Background::Color(match status {
|
|
|
|
|
button::Status::Hovered => ITEM_HOVER,
|
|
|
|
|
_ => Color::TRANSPARENT,
|
|
|
|
|
})),
|
|
|
|
|
text_color: TEXT,
|
|
|
|
|
border: Border {
|
|
|
|
|
color: Color::TRANSPARENT,
|
|
|
|
|
width: 0.0,
|
|
|
|
|
radius: 0.0.into(),
|
|
|
|
|
},
|
|
|
|
|
..Default::default()
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let path_for_remove = path.clone();
|
2026-06-18 12:29:32 +03:00
|
|
|
let remove_btn = button(crate::ui::icons::tinted(crate::ui::icons::CLOSE, 11.0, MUTED))
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
.on_press(Message::RecentRemove(path_for_remove))
|
|
|
|
|
.padding([4, 8])
|
|
|
|
|
.style(|_: &Theme, status| button::Style {
|
|
|
|
|
background: Some(Background::Color(match status {
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
button::Status::Hovered => Color {
|
|
|
|
|
r: 0.45,
|
|
|
|
|
g: 0.15,
|
|
|
|
|
b: 0.15,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
},
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
_ => Color::TRANSPARENT,
|
|
|
|
|
})),
|
|
|
|
|
text_color: MUTED,
|
|
|
|
|
border: Border {
|
|
|
|
|
color: Color::TRANSPARENT,
|
|
|
|
|
width: 0.0,
|
|
|
|
|
radius: 3.0.into(),
|
|
|
|
|
},
|
|
|
|
|
..Default::default()
|
|
|
|
|
});
|
|
|
|
|
|
feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).
Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:
- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
delete / rename) handling handle allocation, ribbon sync, Table re-key
and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
Delete on the left, Set Current + Apply on the right), style list and
chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
starts an inline rename.
New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
MLine apply is a documented placeholder until its editor is editable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
|
|
|
col = col.push(row![open_btn, remove_btn].spacing(0).align_y(iced::Center));
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
}
|
|
|
|
|
iced::widget::scrollable(col).into()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
container(column![header, body])
|
|
|
|
|
.width(iced::Length::Fixed(280.0))
|
|
|
|
|
.height(Fill)
|
|
|
|
|
.style(|_: &Theme| container::Style {
|
|
|
|
|
background: Some(Background::Color(PANEL_BG)),
|
|
|
|
|
border: Border {
|
|
|
|
|
color: PANEL_BORDER,
|
|
|
|
|
width: 1.0,
|
|
|
|
|
radius: 0.0.into(),
|
|
|
|
|
},
|
|
|
|
|
..Default::default()
|
|
|
|
|
})
|
|
|
|
|
.into()
|
|
|
|
|
}
|
feat(dyn-input): TAB-editable distance/angle/coordinate fields
Dinamik giriş artık cursor yanında düzenlenebilir kutular gösteriyor.
TAB ile kutular arası geçilir, bir kutuya yazınca o kutu kilitlenir
(değer sabitlenir), diğerleri cursor'ı takip etmeye devam eder, Enter
ile çözülen nokta komuta point-pick olarak iletilir.
- DocumentTab.dyn_fields / dyn_active state; DynComponent + DynFieldEntry
- sync_dyn_fields komutun dyn_field() + base noktasına göre kutu kümesini
türetir: nokta+base → [d, açı], nokta+base yok → [X, Y], radius → [d],
açı → [açı]
- klavye yönlendirme: rakam/./-/+ aktif kutuya, TAB sonraki kutuya,
Enter try_dyn_commit, Backspace kilidi açar
- dyn_capturing iken command-line text alanı on_input'suz bırakılıyor
ki focus'u ve tuşları çalmasın; girişler global key aboneliğinden
geliyor (numpad dahil text alanı üzerinden yakalanıyor)
- çoklu-kutu overlay render: aktif kutu vurgulu, kilitli kutu farklı
kenar rengi
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:08:57 +03:00
|
|
|
|
2026-05-27 18:05:09 +03:00
|
|
|
// ── Render-mode picker ──────────────────────────────────────────────────────
|
|
|
|
|
|
2026-06-18 23:29:34 +03:00
|
|
|
/// Top-left viewport control bar: a single dark chip holding (optionally) the
|
|
|
|
|
/// horizontal/vertical split buttons, the render-mode picker, and the grid /
|
|
|
|
|
/// grid-snap toggles. `include_split` is off for paper-space viewports, which
|
|
|
|
|
/// have no model-tile splitting. Grid / snap reflect the active viewport's
|
|
|
|
|
/// state and emit `ToggleGrid` / `ToggleGridSnap`.
|
|
|
|
|
fn viewport_controls<'a>(
|
|
|
|
|
render_mode: acadrust::entities::ViewportRenderMode,
|
|
|
|
|
show_grid: bool,
|
|
|
|
|
snap_on: bool,
|
|
|
|
|
include_split: bool,
|
|
|
|
|
) -> Element<'a, Message> {
|
2026-05-27 18:05:09 +03:00
|
|
|
use acadrust::entities::ViewportRenderMode as M;
|
|
|
|
|
let render_modes: Vec<RenderModeChoice> = vec![
|
|
|
|
|
RenderModeChoice(M::Wireframe2D),
|
|
|
|
|
RenderModeChoice(M::Wireframe3D),
|
|
|
|
|
RenderModeChoice(M::HiddenLine),
|
|
|
|
|
RenderModeChoice(M::FlatShaded),
|
|
|
|
|
RenderModeChoice(M::GouraudShaded),
|
|
|
|
|
RenderModeChoice(M::FlatShadedWithEdges),
|
|
|
|
|
RenderModeChoice(M::GouraudShadedWithEdges),
|
|
|
|
|
];
|
2026-06-18 23:29:34 +03:00
|
|
|
let light = Color { r: 0.85, g: 0.85, b: 0.85, a: 1.0 };
|
|
|
|
|
let accent = Color { r: 0.45, g: 0.70, b: 1.0, a: 1.0 };
|
|
|
|
|
|
|
|
|
|
// Borderless icon button; an `active` toggle gets an accent tint + fill.
|
|
|
|
|
let icon_btn = move |bytes: &'static [u8], active: bool, msg: Message| {
|
|
|
|
|
let tint = if active { accent } else { light };
|
|
|
|
|
button(crate::ui::icons::tinted(bytes, 15.0, tint))
|
|
|
|
|
.on_press(msg)
|
|
|
|
|
.padding([4, 6])
|
|
|
|
|
.style(move |_: &Theme, status| iced::widget::button::Style {
|
|
|
|
|
background: Some(Background::Color(match (active, status) {
|
|
|
|
|
(_, iced::widget::button::Status::Hovered) => Color {
|
|
|
|
|
r: 0.25,
|
|
|
|
|
g: 0.25,
|
|
|
|
|
b: 0.25,
|
|
|
|
|
a: 0.9,
|
|
|
|
|
},
|
|
|
|
|
(true, _) => Color {
|
|
|
|
|
r: 0.16,
|
|
|
|
|
g: 0.22,
|
|
|
|
|
b: 0.32,
|
|
|
|
|
a: 0.9,
|
|
|
|
|
},
|
|
|
|
|
(false, _) => Color::TRANSPARENT,
|
|
|
|
|
})),
|
|
|
|
|
border: Border {
|
|
|
|
|
radius: 3.0.into(),
|
|
|
|
|
..Default::default()
|
|
|
|
|
},
|
|
|
|
|
text_color: tint,
|
|
|
|
|
..Default::default()
|
|
|
|
|
})
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Render-mode picker, restyled borderless so the outer chip frames it.
|
|
|
|
|
let picker = iced::widget::pick_list(
|
|
|
|
|
render_modes,
|
|
|
|
|
Some(RenderModeChoice(render_mode)),
|
|
|
|
|
|c| Message::SetRenderMode(c.0),
|
2026-05-27 18:05:09 +03:00
|
|
|
)
|
2026-06-18 23:29:34 +03:00
|
|
|
.text_size(11)
|
|
|
|
|
.padding([4, 6])
|
|
|
|
|
.style(move |_: &Theme, _| iced::widget::pick_list::Style {
|
|
|
|
|
background: Background::Color(Color::TRANSPARENT),
|
|
|
|
|
border: Border {
|
|
|
|
|
radius: 3.0.into(),
|
|
|
|
|
..Default::default()
|
2026-05-27 18:05:09 +03:00
|
|
|
},
|
2026-06-18 23:29:34 +03:00
|
|
|
text_color: light,
|
|
|
|
|
placeholder_color: light,
|
|
|
|
|
handle_color: light,
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-18 23:34:23 +03:00
|
|
|
// Thin vertical divider between control groups.
|
|
|
|
|
let sep = || {
|
|
|
|
|
container(iced::widget::Space::new().width(1.0).height(16.0)).style(|_: &Theme| {
|
|
|
|
|
iced::widget::container::Style {
|
|
|
|
|
background: Some(Background::Color(Color {
|
|
|
|
|
r: 0.45,
|
|
|
|
|
g: 0.45,
|
|
|
|
|
b: 0.45,
|
|
|
|
|
a: 0.7,
|
|
|
|
|
})),
|
|
|
|
|
..Default::default()
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
};
|
|
|
|
|
|
2026-06-18 23:29:34 +03:00
|
|
|
let mut bar = row![]
|
2026-06-18 23:34:23 +03:00
|
|
|
.spacing(3)
|
2026-06-18 23:29:34 +03:00
|
|
|
.align_y(iced::alignment::Vertical::Center);
|
|
|
|
|
if include_split {
|
|
|
|
|
bar = bar
|
|
|
|
|
.push(icon_btn(crate::ui::icons::SPLIT_H, false, Message::SplitModelViewport(true)))
|
2026-06-18 23:34:23 +03:00
|
|
|
.push(sep())
|
|
|
|
|
.push(icon_btn(crate::ui::icons::SPLIT_V, false, Message::SplitModelViewport(false)))
|
|
|
|
|
.push(sep());
|
2026-06-18 23:29:34 +03:00
|
|
|
}
|
|
|
|
|
bar = bar
|
|
|
|
|
.push(picker)
|
2026-06-18 23:34:23 +03:00
|
|
|
.push(sep())
|
2026-06-18 23:29:34 +03:00
|
|
|
.push(icon_btn(crate::ui::icons::GRID, show_grid, Message::ToggleGrid))
|
2026-06-18 23:34:23 +03:00
|
|
|
.push(sep())
|
2026-06-18 23:29:34 +03:00
|
|
|
.push(icon_btn(crate::ui::icons::SNAP, snap_on, Message::ToggleGridSnap));
|
|
|
|
|
|
|
|
|
|
container(bar)
|
|
|
|
|
.padding(2)
|
|
|
|
|
.style(|_: &Theme| iced::widget::container::Style {
|
|
|
|
|
background: Some(Background::Color(Color {
|
|
|
|
|
r: 0.10,
|
|
|
|
|
g: 0.10,
|
|
|
|
|
b: 0.10,
|
|
|
|
|
a: 0.75,
|
|
|
|
|
})),
|
|
|
|
|
border: Border {
|
|
|
|
|
color: Color {
|
|
|
|
|
r: 0.35,
|
|
|
|
|
g: 0.35,
|
|
|
|
|
b: 0.35,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
},
|
|
|
|
|
width: 1.0,
|
|
|
|
|
radius: 4.0.into(),
|
|
|
|
|
},
|
|
|
|
|
..Default::default()
|
|
|
|
|
})
|
|
|
|
|
.into()
|
2026-05-27 18:05:09 +03:00
|
|
|
}
|
|
|
|
|
|
feat(dyn-input): TAB-editable distance/angle/coordinate fields
Dinamik giriş artık cursor yanında düzenlenebilir kutular gösteriyor.
TAB ile kutular arası geçilir, bir kutuya yazınca o kutu kilitlenir
(değer sabitlenir), diğerleri cursor'ı takip etmeye devam eder, Enter
ile çözülen nokta komuta point-pick olarak iletilir.
- DocumentTab.dyn_fields / dyn_active state; DynComponent + DynFieldEntry
- sync_dyn_fields komutun dyn_field() + base noktasına göre kutu kümesini
türetir: nokta+base → [d, açı], nokta+base yok → [X, Y], radius → [d],
açı → [açı]
- klavye yönlendirme: rakam/./-/+ aktif kutuya, TAB sonraki kutuya,
Enter try_dyn_commit, Backspace kilidi açar
- dyn_capturing iken command-line text alanı on_input'suz bırakılıyor
ki focus'u ve tuşları çalmasın; girişler global key aboneliğinden
geliyor (numpad dahil text alanı üzerinden yakalanıyor)
- çoklu-kutu overlay render: aktif kutu vurgulu, kilitli kutu farklı
kenar rengi
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:08:57 +03:00
|
|
|
// ── Dynamic-input field formatting ─────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/// Short prefix shown before a dynamic-input box's value.
|
|
|
|
|
/// The string shown inside a dynamic-input box: the typed buffer when the
|
|
|
|
|
/// field is locked, otherwise the live value derived from the cursor
|
|
|
|
|
/// world position (and the base point for polar quantities).
|
2026-06-22 10:13:47 +03:00
|
|
|
fn dyn_component_value(
|
|
|
|
|
f: &DynFieldEntry,
|
|
|
|
|
w: glam::Vec3,
|
|
|
|
|
base: Option<glam::Vec3>,
|
|
|
|
|
xf: &super::helpers::UcsXform,
|
|
|
|
|
) -> String {
|
feat(dyn-input): TAB-editable distance/angle/coordinate fields
Dinamik giriş artık cursor yanında düzenlenebilir kutular gösteriyor.
TAB ile kutular arası geçilir, bir kutuya yazınca o kutu kilitlenir
(değer sabitlenir), diğerleri cursor'ı takip etmeye devam eder, Enter
ile çözülen nokta komuta point-pick olarak iletilir.
- DocumentTab.dyn_fields / dyn_active state; DynComponent + DynFieldEntry
- sync_dyn_fields komutun dyn_field() + base noktasına göre kutu kümesini
türetir: nokta+base → [d, açı], nokta+base yok → [X, Y], radius → [d],
açı → [açı]
- klavye yönlendirme: rakam/./-/+ aktif kutuya, TAB sonraki kutuya,
Enter try_dyn_commit, Backspace kilidi açar
- dyn_capturing iken command-line text alanı on_input'suz bırakılıyor
ki focus'u ve tuşları çalmasın; girişler global key aboneliğinden
geliyor (numpad dahil text alanı üzerinden yakalanıyor)
- çoklu-kutu overlay render: aktif kutu vurgulu, kilitli kutu farklı
kenar rengi
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:08:57 +03:00
|
|
|
if let Some(b) = &f.buffer {
|
|
|
|
|
return b.clone();
|
|
|
|
|
}
|
|
|
|
|
let b = base.unwrap_or(glam::Vec3::ZERO);
|
2026-06-22 10:13:47 +03:00
|
|
|
// Relative deltas and the polar angle read in the active UCS plane. The
|
|
|
|
|
// delta is offset-invariant, so only the axis rotation matters (identity
|
|
|
|
|
// xf reproduces the world-frame deltas).
|
|
|
|
|
let d = xf.vec_to_ucs(w - b);
|
|
|
|
|
let dx = d.x as f64;
|
|
|
|
|
let dy = d.y as f64;
|
fix(dyn-input): support cartesian coords via "," (#35)
Typing \`,\` while DYN was on appended the character to the focused
field's buffer (treated as a European decimal point), so users
couldn't switch to cartesian coordinates mid-command. The dynamic
input was stuck in polar \`[Distance, Angle]\` whenever a base point
existed.
\`,\` now acts as the coordinate separator (matching the AutoCAD
convention): pressing it locks the current field's buffer, then
either advances within the existing cartesian set or reshapes a
polar configuration into cartesian:
- \`[Distance, Angle]\` + \`,\` on the first field → \`[X(buf), Y]\`
- \`[X, Y]\` + \`,\` on Y → \`[X, Y, Z]\`
- otherwise advances to the next field (Tab-equivalent)
\`sync_dyn_fields\` accepts both polar and cartesian shapes as valid
\`Point\` configurations so a mouse-move after the reshape doesn't
revert to polar. \`dyn_resolve_point\` and the overlay's live-value
formatter treat cartesian inputs as RELATIVE to the base point when
one exists — matching the DYN-on relative-coords convention from
#26. \`DynComponent::Z\` plus its label / value paths fill out the
3-D variant.
Closes #35
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 00:46:18 +03:00
|
|
|
// When a base point exists (DYN-on after the first pick) the cartesian
|
|
|
|
|
// fields show relative deltas — matching the typed-value convention
|
|
|
|
|
// in `dyn_resolve_point` so the live preview and the committed
|
|
|
|
|
// coordinate use the same frame. See #35.
|
|
|
|
|
let has_base = base.is_some();
|
2026-06-15 18:46:34 +03:00
|
|
|
// Width / Height read as unsigned magnitudes (the sign is taken from the
|
|
|
|
|
// cursor side on commit), matching the rectangle's two-edge entry.
|
|
|
|
|
let wh = matches!(f.role, crate::command::DynRole::Width | crate::command::DynRole::Height);
|
feat(dyn-input): TAB-editable distance/angle/coordinate fields
Dinamik giriş artık cursor yanında düzenlenebilir kutular gösteriyor.
TAB ile kutular arası geçilir, bir kutuya yazınca o kutu kilitlenir
(değer sabitlenir), diğerleri cursor'ı takip etmeye devam eder, Enter
ile çözülen nokta komuta point-pick olarak iletilir.
- DocumentTab.dyn_fields / dyn_active state; DynComponent + DynFieldEntry
- sync_dyn_fields komutun dyn_field() + base noktasına göre kutu kümesini
türetir: nokta+base → [d, açı], nokta+base yok → [X, Y], radius → [d],
açı → [açı]
- klavye yönlendirme: rakam/./-/+ aktif kutuya, TAB sonraki kutuya,
Enter try_dyn_commit, Backspace kilidi açar
- dyn_capturing iken command-line text alanı on_input'suz bırakılıyor
ki focus'u ve tuşları çalmasın; girişler global key aboneliğinden
geliyor (numpad dahil text alanı üzerinden yakalanıyor)
- çoklu-kutu overlay render: aktif kutu vurgulu, kilitli kutu farklı
kenar rengi
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:08:57 +03:00
|
|
|
match f.component {
|
2026-06-15 18:46:34 +03:00
|
|
|
DynComponent::X if has_base => format!("{:.4}", if wh { dx.abs() } else { dx }),
|
|
|
|
|
DynComponent::Y if has_base => format!("{:.4}", if wh { dy.abs() } else { dy }),
|
fix(dyn-input): support cartesian coords via "," (#35)
Typing \`,\` while DYN was on appended the character to the focused
field's buffer (treated as a European decimal point), so users
couldn't switch to cartesian coordinates mid-command. The dynamic
input was stuck in polar \`[Distance, Angle]\` whenever a base point
existed.
\`,\` now acts as the coordinate separator (matching the AutoCAD
convention): pressing it locks the current field's buffer, then
either advances within the existing cartesian set or reshapes a
polar configuration into cartesian:
- \`[Distance, Angle]\` + \`,\` on the first field → \`[X(buf), Y]\`
- \`[X, Y]\` + \`,\` on Y → \`[X, Y, Z]\`
- otherwise advances to the next field (Tab-equivalent)
\`sync_dyn_fields\` accepts both polar and cartesian shapes as valid
\`Point\` configurations so a mouse-move after the reshape doesn't
revert to polar. \`dyn_resolve_point\` and the overlay's live-value
formatter treat cartesian inputs as RELATIVE to the base point when
one exists — matching the DYN-on relative-coords convention from
#26. \`DynComponent::Z\` plus its label / value paths fill out the
3-D variant.
Closes #35
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 00:46:18 +03:00
|
|
|
DynComponent::Z if has_base => "0.0000".to_string(),
|
feat(dyn-input): TAB-editable distance/angle/coordinate fields
Dinamik giriş artık cursor yanında düzenlenebilir kutular gösteriyor.
TAB ile kutular arası geçilir, bir kutuya yazınca o kutu kilitlenir
(değer sabitlenir), diğerleri cursor'ı takip etmeye devam eder, Enter
ile çözülen nokta komuta point-pick olarak iletilir.
- DocumentTab.dyn_fields / dyn_active state; DynComponent + DynFieldEntry
- sync_dyn_fields komutun dyn_field() + base noktasına göre kutu kümesini
türetir: nokta+base → [d, açı], nokta+base yok → [X, Y], radius → [d],
açı → [açı]
- klavye yönlendirme: rakam/./-/+ aktif kutuya, TAB sonraki kutuya,
Enter try_dyn_commit, Backspace kilidi açar
- dyn_capturing iken command-line text alanı on_input'suz bırakılıyor
ki focus'u ve tuşları çalmasın; girişler global key aboneliğinden
geliyor (numpad dahil text alanı üzerinden yakalanıyor)
- çoklu-kutu overlay render: aktif kutu vurgulu, kilitli kutu farklı
kenar rengi
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:08:57 +03:00
|
|
|
DynComponent::X => format!("{:.4}", w.x),
|
|
|
|
|
DynComponent::Y => format!("{:.4}", w.y),
|
fix(dyn-input): support cartesian coords via "," (#35)
Typing \`,\` while DYN was on appended the character to the focused
field's buffer (treated as a European decimal point), so users
couldn't switch to cartesian coordinates mid-command. The dynamic
input was stuck in polar \`[Distance, Angle]\` whenever a base point
existed.
\`,\` now acts as the coordinate separator (matching the AutoCAD
convention): pressing it locks the current field's buffer, then
either advances within the existing cartesian set or reshapes a
polar configuration into cartesian:
- \`[Distance, Angle]\` + \`,\` on the first field → \`[X(buf), Y]\`
- \`[X, Y]\` + \`,\` on Y → \`[X, Y, Z]\`
- otherwise advances to the next field (Tab-equivalent)
\`sync_dyn_fields\` accepts both polar and cartesian shapes as valid
\`Point\` configurations so a mouse-move after the reshape doesn't
revert to polar. \`dyn_resolve_point\` and the overlay's live-value
formatter treat cartesian inputs as RELATIVE to the base point when
one exists — matching the DYN-on relative-coords convention from
#26. \`DynComponent::Z\` plus its label / value paths fill out the
3-D variant.
Closes #35
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 00:46:18 +03:00
|
|
|
DynComponent::Z => format!("{:.4}", b.z),
|
2026-06-15 17:12:08 +03:00
|
|
|
// Scaled by the role so a diameter box reads twice the radius.
|
|
|
|
|
DynComponent::Distance => {
|
|
|
|
|
format!("{:.4}", (dx * dx + dy * dy).sqrt() * f.role.value_scale() as f64)
|
|
|
|
|
}
|
2026-06-15 23:03:08 +03:00
|
|
|
// Shared rule: unsigned magnitude of the short angle, so CW (below the
|
|
|
|
|
// reference axis) reads positive (e.g. 30°, not -30°/330°). The
|
|
|
|
|
// committed value stays signed (see dyn_resolve_point).
|
|
|
|
|
DynComponent::Angle => {
|
|
|
|
|
format!("{:.1}", crate::command::dyn_display_angle_deg(dy.atan2(dx) as f32))
|
|
|
|
|
}
|
2026-06-09 18:32:03 +03:00
|
|
|
// Typed-only scalar — no geometric value to track when empty.
|
|
|
|
|
DynComponent::Scalar => String::new(),
|
feat(dyn-input): TAB-editable distance/angle/coordinate fields
Dinamik giriş artık cursor yanında düzenlenebilir kutular gösteriyor.
TAB ile kutular arası geçilir, bir kutuya yazınca o kutu kilitlenir
(değer sabitlenir), diğerleri cursor'ı takip etmeye devam eder, Enter
ile çözülen nokta komuta point-pick olarak iletilir.
- DocumentTab.dyn_fields / dyn_active state; DynComponent + DynFieldEntry
- sync_dyn_fields komutun dyn_field() + base noktasına göre kutu kümesini
türetir: nokta+base → [d, açı], nokta+base yok → [X, Y], radius → [d],
açı → [açı]
- klavye yönlendirme: rakam/./-/+ aktif kutuya, TAB sonraki kutuya,
Enter try_dyn_commit, Backspace kilidi açar
- dyn_capturing iken command-line text alanı on_input'suz bırakılıyor
ki focus'u ve tuşları çalmasın; girişler global key aboneliğinden
geliyor (numpad dahil text alanı üzerinden yakalanıyor)
- çoklu-kutu overlay render: aktif kutu vurgulu, kilitli kutu farklı
kenar rengi
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:08:57 +03:00
|
|
|
}
|
|
|
|
|
}
|