2026-03-22 22:44:32 +03:00
|
|
|
|
use super::document::DocumentTab;
|
|
|
|
|
|
use super::helpers::grid_plane_from_camera;
|
2026-05-12 10:36:15 +03:00
|
|
|
|
use super::history::history_dropdown_labels;
|
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
|
|
|
|
use super::document::{DynComponent, DynFieldEntry};
|
2026-05-23 17:16:15 +03:00
|
|
|
|
use super::{Message, OpenCADStudio};
|
2026-05-01 12:16:46 +03:00
|
|
|
|
use crate::scene::grip::{grips_to_screen, grips_to_screen_paper};
|
2026-04-23 09:48:53 +03:00
|
|
|
|
use crate::scene::paper_canvas::PaperCanvas;
|
2026-05-28 10:03:01 +03:00
|
|
|
|
use crate::scene::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::{
|
|
|
|
|
|
button, canvas, column, container, mouse_area, pick_list, row, shader, stack, text, text_input,
|
|
|
|
|
|
Row, Space,
|
|
|
|
|
|
};
|
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-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-03-22 22:44:32 +03:00
|
|
|
|
if Some(window_id) == self.layer_window {
|
|
|
|
|
|
let tab = &self.tabs[self.active_tab];
|
|
|
|
|
|
return tab.layers.view_window();
|
|
|
|
|
|
}
|
2026-04-08 22:36:28 +03:00
|
|
|
|
if Some(window_id) == self.page_setup_window {
|
2026-04-08 22:56:51 +03:00
|
|
|
|
return crate::ui::page_setup::view_window(
|
2026-05-12 10:36:15 +03:00
|
|
|
|
&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,
|
2026-04-08 22:36:28 +03:00
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
if Some(window_id) == self.textstyle_window {
|
|
|
|
|
|
let tab = &self.tabs[self.active_tab];
|
2026-05-12 10:36:15 +03:00
|
|
|
|
let styles: Vec<String> = tab
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.document
|
|
|
|
|
|
.text_styles
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.map(|s| s.name.clone())
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
return crate::ui::textstyle::view_window(
|
|
|
|
|
|
styles,
|
|
|
|
|
|
&self.textstyle_selected,
|
|
|
|
|
|
&self.textstyle_font,
|
|
|
|
|
|
&self.textstyle_width,
|
|
|
|
|
|
&self.textstyle_oblique,
|
|
|
|
|
|
);
|
2026-04-08 22:36:28 +03:00
|
|
|
|
}
|
|
|
|
|
|
if Some(window_id) == self.tablestyle_window {
|
|
|
|
|
|
use acadrust::objects::ObjectType;
|
|
|
|
|
|
let tab = &self.tabs[self.active_tab];
|
2026-05-12 10:36:15 +03:00
|
|
|
|
let styles: Vec<String> = tab
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.document
|
|
|
|
|
|
.objects
|
|
|
|
|
|
.values()
|
|
|
|
|
|
.filter_map(|o| {
|
|
|
|
|
|
if let ObjectType::TableStyle(s) = o {
|
|
|
|
|
|
Some(s.name.clone())
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
2026-04-08 22:36:28 +03:00
|
|
|
|
.collect();
|
2026-05-12 10:36:15 +03:00
|
|
|
|
let selected_style = tab.scene.document.objects.values().find_map(|o| {
|
|
|
|
|
|
if let ObjectType::TableStyle(s) = o {
|
|
|
|
|
|
if s.name == self.tablestyle_selected {
|
|
|
|
|
|
Some(s)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
return crate::ui::tablestyle::view_window(
|
|
|
|
|
|
styles,
|
|
|
|
|
|
&self.tablestyle_selected,
|
|
|
|
|
|
selected_style,
|
|
|
|
|
|
);
|
2026-04-08 22:36:28 +03:00
|
|
|
|
}
|
|
|
|
|
|
if Some(window_id) == self.mlstyle_window {
|
|
|
|
|
|
use acadrust::objects::ObjectType;
|
|
|
|
|
|
let tab = &self.tabs[self.active_tab];
|
2026-05-12 10:36:15 +03:00
|
|
|
|
let styles: Vec<String> = tab
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.document
|
|
|
|
|
|
.objects
|
|
|
|
|
|
.values()
|
|
|
|
|
|
.filter_map(|o| {
|
|
|
|
|
|
if let ObjectType::MLineStyle(s) = o {
|
|
|
|
|
|
Some(s.name.clone())
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
2026-04-08 22:36:28 +03:00
|
|
|
|
.collect();
|
2026-05-12 10:36:15 +03:00
|
|
|
|
let selected_style = tab.scene.document.objects.values().find_map(|o| {
|
|
|
|
|
|
if let ObjectType::MLineStyle(s) = o {
|
|
|
|
|
|
if s.name == self.mlstyle_selected {
|
|
|
|
|
|
Some(s)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
return crate::ui::mlstyle::view_window(
|
|
|
|
|
|
styles,
|
|
|
|
|
|
&self.mlstyle_selected,
|
|
|
|
|
|
selected_style,
|
|
|
|
|
|
tab.scene.document.header.multiline_style.clone(),
|
|
|
|
|
|
);
|
2026-04-08 22:36:28 +03:00
|
|
|
|
}
|
|
|
|
|
|
if Some(window_id) == self.layout_manager_window {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let layouts = self.tabs[i].scene.layout_names();
|
|
|
|
|
|
let current = self.tabs[i].scene.current_layout.clone();
|
2026-05-12 10:36:15 +03:00
|
|
|
|
return crate::ui::layout_manager::view_window(
|
|
|
|
|
|
layouts,
|
|
|
|
|
|
&self.layout_manager_selected,
|
|
|
|
|
|
&self.layout_manager_rename_buf,
|
|
|
|
|
|
current,
|
|
|
|
|
|
);
|
2026-04-08 22:36:28 +03:00
|
|
|
|
}
|
|
|
|
|
|
if Some(window_id) == self.plotstyle_window {
|
2026-04-08 22:56:51 +03:00
|
|
|
|
return crate::ui::plotstyle::view_window(
|
2026-05-12 10:36:15 +03:00
|
|
|
|
self.active_plot_style.as_ref(),
|
|
|
|
|
|
self.plotstyle_panel_aci,
|
|
|
|
|
|
&self.ps_color_buf,
|
|
|
|
|
|
&self.ps_lineweight_buf,
|
|
|
|
|
|
&self.ps_screening_buf,
|
2026-04-08 22:36:28 +03:00
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
if Some(window_id) == self.dimstyle_window {
|
|
|
|
|
|
let tab = &self.tabs[self.active_tab];
|
2026-05-12 10:36:15 +03:00
|
|
|
|
let styles: Vec<String> = tab
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.document
|
|
|
|
|
|
.dim_styles
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.map(|s| s.name.clone())
|
|
|
|
|
|
.collect();
|
2026-04-08 22:56:51 +03:00
|
|
|
|
return crate::ui::dimstyle::view_window(
|
2026-05-12 10:36:15 +03:00
|
|
|
|
styles,
|
|
|
|
|
|
&self.dimstyle_selected,
|
|
|
|
|
|
self.dimstyle_tab,
|
2026-04-08 22:56:51 +03:00
|
|
|
|
crate::ui::dimstyle::DimStyleValues {
|
2026-05-12 10:36:15 +03:00
|
|
|
|
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,
|
2026-04-08 22:56:51 +03:00
|
|
|
|
},
|
|
|
|
|
|
);
|
2026-04-08 22:36:28 +03:00
|
|
|
|
}
|
|
|
|
|
|
if Some(window_id) == self.shortcuts_window {
|
2026-04-08 22:56:51 +03:00
|
|
|
|
return crate::ui::shortcuts::view_window(&self.shortcut_overrides);
|
2026-04-08 22:36:28 +03:00
|
|
|
|
}
|
2026-04-22 22:22:33 +03:00
|
|
|
|
if Some(window_id) == self.about_window {
|
|
|
|
|
|
return crate::ui::about::view_window();
|
|
|
|
|
|
}
|
2026-05-16 19:55:57 +03:00
|
|
|
|
if Some(window_id) == self.update_notice_window {
|
|
|
|
|
|
let latest = self
|
|
|
|
|
|
.update_notice_version
|
|
|
|
|
|
.as_deref()
|
|
|
|
|
|
.unwrap_or("?");
|
2026-05-22 12:55:37 +03:00
|
|
|
|
let body = self.update_notice_body.as_deref().unwrap_or("");
|
|
|
|
|
|
return crate::ui::update_notice::view_window(latest, body);
|
2026-05-16 19:55:57 +03:00
|
|
|
|
}
|
2026-05-01 11:31:11 +03:00
|
|
|
|
if Some(window_id) == self.unsaved_dialog_window {
|
|
|
|
|
|
let tab_name = match &self.pending_close {
|
|
|
|
|
|
Some(super::PendingClose::Tab(idx)) => self
|
2026-05-12 10:36:15 +03:00
|
|
|
|
.tabs
|
|
|
|
|
|
.get(*idx)
|
2026-05-01 11:31:11 +03:00
|
|
|
|
.map(|t| t.tab_display_name())
|
|
|
|
|
|
.unwrap_or_default(),
|
|
|
|
|
|
Some(super::PendingClose::Quit) => self
|
2026-05-12 10:36:15 +03:00
|
|
|
|
.tabs
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.find(|t| t.dirty)
|
2026-05-01 11:31:11 +03:00
|
|
|
|
.map(|t| t.tab_display_name())
|
|
|
|
|
|
.unwrap_or_default(),
|
|
|
|
|
|
None => String::new(),
|
|
|
|
|
|
};
|
|
|
|
|
|
return unsaved_changes_dialog_window(&tab_name);
|
|
|
|
|
|
}
|
2026-05-01 16:12:32 +03:00
|
|
|
|
if Some(window_id) == self.save_dialog_window {
|
|
|
|
|
|
return save_as_dialog_window(
|
|
|
|
|
|
&self.save_dialog_filename,
|
|
|
|
|
|
&self.save_dialog_folder,
|
2026-05-01 16:20:38 +03:00
|
|
|
|
&self.save_dialog_entries,
|
2026-05-01 16:12:32 +03:00
|
|
|
|
&self.save_dialog_format,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
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";
|
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: render welcome page in place of the model/paper canvas.
|
|
|
|
|
|
// 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-05-28 10:03:01 +03:00
|
|
|
|
// Unified GPU widget for both layouts. In a paper layout the
|
|
|
|
|
|
// PaperCanvas (2-D sheet + paper entities + viewport borders) is
|
|
|
|
|
|
// layered underneath this shader so the area outside floating
|
|
|
|
|
|
// viewports shows the sheet; the shader only blits inside each
|
|
|
|
|
|
// content viewport's scissor rect.
|
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,
|
|
|
|
|
|
))
|
2026-04-23 02:18:41 +03:00
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill)
|
|
|
|
|
|
.into()
|
|
|
|
|
|
};
|
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;
|
|
|
|
|
|
let tx = cam.target.x;
|
|
|
|
|
|
let ty = cam.target.y;
|
|
|
|
|
|
drop(cam);
|
|
|
|
|
|
grips_to_screen_paper(&tab.selected_grips, tx, ty, half_w, half_h, bounds)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let vp_mat = tab.scene.camera.borrow().view_proj(bounds);
|
|
|
|
|
|
grips_to_screen(&tab.selected_grips, vp_mat, bounds)
|
|
|
|
|
|
};
|
|
|
|
|
|
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-03-22 22:44:32 +03:00
|
|
|
|
let grid = if self.show_grid {
|
|
|
|
|
|
let cam = tab.scene.camera.borrow();
|
|
|
|
|
|
let plane = grid_plane_from_camera(cam.pitch, cam.yaw);
|
|
|
|
|
|
Some(overlay::GridParams {
|
2026-04-03 19:25:55 +03:00
|
|
|
|
view_proj: cam.view_proj(vp_bounds),
|
|
|
|
|
|
bounds: vp_bounds,
|
2026-03-22 22:44:32 +03:00
|
|
|
|
plane,
|
|
|
|
|
|
})
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
};
|
2026-04-03 19:25:55 +03:00
|
|
|
|
|
|
|
|
|
|
let ucs_icon = if self.show_ucs_icon && !is_paper {
|
|
|
|
|
|
let cam = tab.scene.camera.borrow();
|
|
|
|
|
|
Some(overlay::UcsIconParams {
|
|
|
|
|
|
view_proj: cam.view_proj(vp_bounds),
|
|
|
|
|
|
bounds: vp_bounds,
|
|
|
|
|
|
})
|
|
|
|
|
|
} 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
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-27 00:08:37 +03:00
|
|
|
|
// Render-mode picker, top-left of the viewport. Drives whether
|
|
|
|
|
|
// 3D face / mesh fills and edges are rendered for the active
|
|
|
|
|
|
// tab. Hatch fills are deliberately *not* gated by this — the
|
|
|
|
|
|
// document's FILLMODE still owns 2D fill state.
|
2026-05-27 18:05:09 +03:00
|
|
|
|
let info = render_mode_picker(tab.render_mode);
|
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-04-08 00:12:09 +03:00
|
|
|
|
let dyn_input_overlay: Option<Element<'_, Message>> =
|
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 self.dyn_input && tab.active_cmd.is_some() && !tab.dyn_fields.is_empty() {
|
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;
|
|
|
|
|
|
let boxes: Vec<overlay::DynBox> = tab
|
|
|
|
|
|
.dyn_fields
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.enumerate()
|
|
|
|
|
|
.map(|(idx, f)| overlay::DynBox {
|
|
|
|
|
|
label: dyn_component_label(f.component),
|
|
|
|
|
|
value: dyn_component_value(f, w, base),
|
|
|
|
|
|
active: idx == tab.dyn_active,
|
|
|
|
|
|
locked: f.locked(),
|
|
|
|
|
|
})
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
Some(overlay::dynamic_input_overlay(tab.last_cursor_screen, boxes))
|
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 {
|
|
|
|
|
|
// Paper layout: 2-D sheet + paper entities + viewport borders
|
|
|
|
|
|
// underneath, unified GPU shader on top (only paints inside
|
|
|
|
|
|
// each content viewport's scissor rect so the sheet shows
|
|
|
|
|
|
// through outside them).
|
|
|
|
|
|
let paper_sheet: Element<'_, Message> =
|
|
|
|
|
|
canvas(PaperCanvas::new(&tab.scene)).width(Fill).height(Fill).into();
|
|
|
|
|
|
stack![
|
|
|
|
|
|
paper_sheet,
|
|
|
|
|
|
container(viewport_3d).width(Fill).height(Fill),
|
|
|
|
|
|
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-05-27 22:33:36 +03:00
|
|
|
|
// Render-mode picker + viewport-split buttons (horizontal /
|
|
|
|
|
|
// vertical divider of the active Model tile).
|
|
|
|
|
|
let split_btn = |glyph: &'static str, horizontal: bool| {
|
|
|
|
|
|
button(text(glyph).size(13).color(Color {
|
|
|
|
|
|
r: 0.85,
|
|
|
|
|
|
g: 0.85,
|
|
|
|
|
|
b: 0.85,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
}))
|
|
|
|
|
|
.on_press(Message::SplitModelViewport(horizontal))
|
|
|
|
|
|
.padding([4, 8])
|
|
|
|
|
|
.style(|_: &Theme, status| iced::widget::button::Style {
|
|
|
|
|
|
background: Some(Background::Color(match status {
|
|
|
|
|
|
iced::widget::button::Status::Hovered => Color {
|
|
|
|
|
|
r: 0.20,
|
|
|
|
|
|
g: 0.20,
|
|
|
|
|
|
b: 0.20,
|
|
|
|
|
|
a: 0.85,
|
|
|
|
|
|
},
|
|
|
|
|
|
_ => 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(),
|
|
|
|
|
|
},
|
|
|
|
|
|
text_color: Color {
|
|
|
|
|
|
r: 0.85,
|
|
|
|
|
|
g: 0.85,
|
|
|
|
|
|
b: 0.85,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
},
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
})
|
|
|
|
|
|
};
|
|
|
|
|
|
let bar = row![info, split_btn("▤", true), split_btn("▥", false)].spacing(4);
|
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
|
|
|
|
|
|
// clicks (the paper canvas itself sits below it). Positioned with
|
|
|
|
|
|
// 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)),
|
|
|
|
|
|
iced::widget::opaque(render_mode_picker(vp_mode)),
|
|
|
|
|
|
],
|
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 {
|
|
|
|
|
|
let anchor_x = (popup.anchor.x + 12.0).max(0.0);
|
|
|
|
|
|
let anchor_y = (popup.anchor.y + 12.0).max(0.0);
|
|
|
|
|
|
let mut col = column![].spacing(0);
|
|
|
|
|
|
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)
|
|
|
|
|
|
.width(iced::Length::Shrink)
|
|
|
|
|
|
.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()
|
|
|
|
|
|
});
|
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
|
|
|
|
let popup_layer = iced::widget::column![
|
|
|
|
|
|
Space::new().height(iced::Length::Fixed(anchor_y)),
|
|
|
|
|
|
iced::widget::row![
|
|
|
|
|
|
Space::new().width(iced::Length::Fixed(anchor_x)),
|
2026-05-29 09:51:12 +03:00
|
|
|
|
iced::widget::opaque(menu_panel),
|
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
|
|
|
|
],
|
|
|
|
|
|
]
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill);
|
|
|
|
|
|
viewport_stack = viewport_stack.push(popup_layer);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
} else if self.show_properties {
|
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.
|
|
|
|
|
|
let dyn_capturing =
|
|
|
|
|
|
self.dyn_input && tab.active_cmd.is_some() && !tab.dyn_fields.is_empty();
|
|
|
|
|
|
let command_line_overlay =
|
|
|
|
|
|
iced::widget::container(self.command_line.view(allow_autocomplete, dyn_capturing))
|
2026-05-26 19:23:19 +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,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
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({
|
|
|
|
|
|
let mut col = column![self.ribbon.view(
|
|
|
|
|
|
is_paper,
|
|
|
|
|
|
self.tabs[self.active_tab].history.undo_stack.len(),
|
|
|
|
|
|
self.tabs[self.active_tab].history.redo_stack.len(),
|
|
|
|
|
|
)];
|
|
|
|
|
|
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();
|
|
|
|
|
|
self.status_bar.view(
|
|
|
|
|
|
&self.snapper,
|
|
|
|
|
|
self.snap_popup_open,
|
|
|
|
|
|
self.ortho_mode,
|
|
|
|
|
|
self.polar_mode,
|
|
|
|
|
|
self.polar_increment_deg,
|
|
|
|
|
|
self.show_grid,
|
|
|
|
|
|
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-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(),
|
|
|
|
|
|
)
|
|
|
|
|
|
} 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-04-08 00:41:01 +03:00
|
|
|
|
// ── Viewport right-click context menu ─────────────────────────────
|
|
|
|
|
|
let viewport_ctx_layer: Element<'_, Message> = {
|
|
|
|
|
|
let ctx_pos = tab.scene.selection.borrow().context_menu;
|
|
|
|
|
|
if let Some(p) = ctx_pos {
|
|
|
|
|
|
let has_cmd = tab.active_cmd.is_some();
|
|
|
|
|
|
let has_selection = !tab.scene.selected.is_empty();
|
2026-05-12 10:36:15 +03:00
|
|
|
|
let last_cmds: Vec<String> = self
|
|
|
|
|
|
.command_line
|
|
|
|
|
|
.cmd_recall
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.rev()
|
|
|
|
|
|
.take(3)
|
|
|
|
|
|
.cloned()
|
|
|
|
|
|
.collect();
|
2026-04-08 00:41:01 +03:00
|
|
|
|
viewport_context_menu_overlay(p, has_cmd, has_selection, last_cmds)
|
|
|
|
|
|
} 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-05-12 10:36:15 +03:00
|
|
|
|
stack![
|
|
|
|
|
|
main_ui,
|
|
|
|
|
|
self.app_menu.view(),
|
|
|
|
|
|
snap_layer,
|
|
|
|
|
|
scale_layer,
|
|
|
|
|
|
dropdown_layer,
|
|
|
|
|
|
layout_ctx_layer,
|
2026-05-13 09:59:07 +03:00
|
|
|
|
viewport_ctx_layer,
|
|
|
|
|
|
open_progress_layer,
|
2026-05-12 10:36:15 +03:00
|
|
|
|
]
|
|
|
|
|
|
.into()
|
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-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-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))
|
|
|
|
|
|
}
|
|
|
|
|
|
iced::Event::Keyboard(keyboard::Event::KeyPressed {
|
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
|
|
|
|
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 {
|
|
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::Enter)
|
|
|
|
|
|
| keyboard::Key::Named(keyboard::key::Named::Space)
|
|
|
|
|
|
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-04-07 00:04:12 +03:00
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::ArrowUp) => {
|
|
|
|
|
|
Some(Message::CommandHistoryPrev)
|
|
|
|
|
|
}
|
|
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::ArrowDown) => {
|
|
|
|
|
|
Some(Message::CommandHistoryNext)
|
|
|
|
|
|
}
|
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())),
|
|
|
|
|
|
"v" => Some(Message::Command("PASTECLIP".to_string())),
|
|
|
|
|
|
_ => 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;
|
|
|
|
|
|
let name = tab.tab_display_name();
|
2026-05-12 10:36:15 +03:00
|
|
|
|
let label = if tab.dirty {
|
|
|
|
|
|
format!("● {}", name)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
name
|
|
|
|
|
|
};
|
2026-03-22 22:44:32 +03:00
|
|
|
|
|
|
|
|
|
|
let title_btn = button(text(label).size(12))
|
|
|
|
|
|
.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 {
|
|
|
|
|
|
let close_btn = button(text("×").size(11).color(Color {
|
|
|
|
|
|
r: 0.55,
|
|
|
|
|
|
g: 0.55,
|
|
|
|
|
|
b: 0.55,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
}))
|
|
|
|
|
|
.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 {
|
|
|
|
|
|
color: if is_active { BORDER_COLOR } else { Color::TRANSPARENT },
|
|
|
|
|
|
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-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,
|
|
|
|
|
|
last_cmds: Vec<String>,
|
|
|
|
|
|
) -> 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()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
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-05-12 10:36:15 +03:00
|
|
|
|
items.push(item(
|
|
|
|
|
|
"Select All".to_string(),
|
|
|
|
|
|
Message::Command("SELECTALL".to_string()),
|
|
|
|
|
|
));
|
|
|
|
|
|
items.push(item(
|
|
|
|
|
|
"Zoom Extents".to_string(),
|
|
|
|
|
|
Message::Command("ZOOM".to_string()),
|
|
|
|
|
|
));
|
2026-04-08 00:41:01 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let menu_col = column(items).spacing(0).width(180);
|
|
|
|
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
|
})
|
|
|
|
|
|
.padding([4, 0]);
|
|
|
|
|
|
|
|
|
|
|
|
// Click-catcher to close the menu when clicking outside.
|
2026-05-12 10:36:15 +03:00
|
|
|
|
let catcher = mouse_area(container(Space::new()).width(Fill).height(Fill))
|
|
|
|
|
|
.on_press(Message::ViewportContextMenuClose)
|
|
|
|
|
|
.on_right_press(Message::ViewportContextMenuClose);
|
2026-04-08 00:41:01 +03:00
|
|
|
|
|
|
|
|
|
|
// Position using top/left spacing.
|
|
|
|
|
|
let positioned = column![
|
|
|
|
|
|
Space::new().height(pos.y),
|
|
|
|
|
|
row![Space::new().width(pos.x), menu],
|
|
|
|
|
|
]
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill);
|
|
|
|
|
|
|
|
|
|
|
|
stack![catcher, positioned].into()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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-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);
|
|
|
|
|
|
let b = button(text("↑").size(14).color(TEXT))
|
|
|
|
|
|
.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)| {
|
|
|
|
|
|
let icon = if *is_dir { "📁" } else { "📄" };
|
|
|
|
|
|
let color = if *is_dir { DIR_COL } else { FILE_COL };
|
|
|
|
|
|
let p = path.clone();
|
|
|
|
|
|
let d = *is_dir;
|
|
|
|
|
|
mouse_area(
|
|
|
|
|
|
container(
|
|
|
|
|
|
row![
|
|
|
|
|
|
text(icon).size(13),
|
|
|
|
|
|
Space::new().width(6),
|
|
|
|
|
|
text(name.as_str()).size(13).color(color),
|
|
|
|
|
|
]
|
|
|
|
|
|
.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
|
|
|
|
|
|
|
|
|
|
// ── 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
|
|
|
|
|
|
|
|
|
|
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 };
|
|
|
|
|
|
|
|
|
|
|
|
pub(super) fn start_page_view<'a>() -> Element<'a, Message> {
|
|
|
|
|
|
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 };
|
|
|
|
|
|
|
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 {
|
|
|
|
|
|
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 },
|
|
|
|
|
|
})),
|
|
|
|
|
|
text_color: TEXT,
|
|
|
|
|
|
border: Border {
|
|
|
|
|
|
color: Color { r: 0.30, g: 0.30, b: 0.33, a: 1.0 },
|
|
|
|
|
|
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![
|
|
|
|
|
|
text("♥ ").size(15).color(Color::WHITE),
|
|
|
|
|
|
text("Donate").size(14).color(Color::WHITE),
|
|
|
|
|
|
]
|
|
|
|
|
|
.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 {
|
|
|
|
|
|
color: Color { r: 0.0, g: 0.0, b: 0.0, a: 0.4 },
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
|
|
let secondary_row = row![
|
|
|
|
|
|
outline_btn(
|
|
|
|
|
|
"Contribute",
|
|
|
|
|
|
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()),
|
|
|
|
|
|
},
|
|
|
|
|
|
),
|
|
|
|
|
|
outline_btn("About", Message::AboutOpen),
|
|
|
|
|
|
]
|
|
|
|
|
|
.spacing(12)
|
|
|
|
|
|
.align_y(iced::Center);
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
.padding(iced::Padding { top: 4.0, right: 8.0, bottom: 4.0, left: 8.0 })
|
|
|
|
|
|
.style(|_: &Theme| container::Style {
|
|
|
|
|
|
background: Some(Background::Color(Color::TRANSPARENT)),
|
|
|
|
|
|
shadow: iced::Shadow {
|
|
|
|
|
|
color: Color { r: BRAND.r, g: BRAND.g, b: BRAND.b, a: 0.45 },
|
|
|
|
|
|
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".
|
|
|
|
|
|
const PAGE_BG: Color = Color { r: 0.08, g: 0.08, b: 0.085, a: 1.0 };
|
|
|
|
|
|
container(content)
|
|
|
|
|
|
.style(|_: &Theme| container::Style {
|
|
|
|
|
|
background: Some(Background::Color(PAGE_BG)),
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
})
|
|
|
|
|
|
.padding(iced::Padding { top: 40.0, right: 60.0, bottom: 40.0, left: 60.0 })
|
|
|
|
|
|
.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> {
|
|
|
|
|
|
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 };
|
|
|
|
|
|
|
|
|
|
|
|
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 });
|
|
|
|
|
|
|
|
|
|
|
|
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![
|
|
|
|
|
|
text(name).size(12).color(TEXT),
|
|
|
|
|
|
text(dir).size(10).color(MUTED),
|
|
|
|
|
|
]
|
|
|
|
|
|
.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();
|
|
|
|
|
|
let remove_btn = button(text("×").size(12).color(MUTED))
|
|
|
|
|
|
.on_press(Message::RecentRemove(path_for_remove))
|
|
|
|
|
|
.padding([4, 8])
|
|
|
|
|
|
.style(|_: &Theme, status| button::Style {
|
|
|
|
|
|
background: Some(Background::Color(match status {
|
|
|
|
|
|
button::Status::Hovered => Color { r: 0.45, g: 0.15, b: 0.15, a: 1.0 },
|
|
|
|
|
|
_ => Color::TRANSPARENT,
|
|
|
|
|
|
})),
|
|
|
|
|
|
text_color: MUTED,
|
|
|
|
|
|
border: Border {
|
|
|
|
|
|
color: Color::TRANSPARENT,
|
|
|
|
|
|
width: 0.0,
|
|
|
|
|
|
radius: 3.0.into(),
|
|
|
|
|
|
},
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
col = col.push(
|
|
|
|
|
|
row![open_btn, remove_btn]
|
|
|
|
|
|
.spacing(0)
|
|
|
|
|
|
.align_y(iced::Center),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
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 ──────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
/// The 7-way visual-style `pick_list`, styled as a dark overlay chip.
|
|
|
|
|
|
/// Shared by the model-space viewport (top-left) and each active
|
|
|
|
|
|
/// paper-space viewport. Emits `SetRenderMode`, which the update loop
|
|
|
|
|
|
/// routes to the active viewport entity or the model-layout tab.
|
|
|
|
|
|
fn render_mode_picker<'a>(
|
|
|
|
|
|
current: acadrust::entities::ViewportRenderMode,
|
|
|
|
|
|
) -> Element<'a, Message> {
|
|
|
|
|
|
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),
|
|
|
|
|
|
];
|
|
|
|
|
|
container(
|
|
|
|
|
|
iced::widget::pick_list(render_modes, Some(RenderModeChoice(current)), |c| {
|
|
|
|
|
|
Message::SetRenderMode(c.0)
|
|
|
|
|
|
})
|
|
|
|
|
|
.text_size(11),
|
|
|
|
|
|
)
|
|
|
|
|
|
.style(|_: &Theme| iced::widget::container::Style {
|
|
|
|
|
|
background: Some(iced::Background::Color(Color {
|
|
|
|
|
|
r: 0.10,
|
|
|
|
|
|
g: 0.10,
|
|
|
|
|
|
b: 0.10,
|
|
|
|
|
|
a: 0.75,
|
|
|
|
|
|
})),
|
|
|
|
|
|
border: iced::Border {
|
|
|
|
|
|
color: Color {
|
|
|
|
|
|
r: 0.35,
|
|
|
|
|
|
g: 0.35,
|
|
|
|
|
|
b: 0.35,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
},
|
|
|
|
|
|
width: 1.0,
|
|
|
|
|
|
radius: 4.0.into(),
|
|
|
|
|
|
},
|
|
|
|
|
|
text_color: Some(Color {
|
|
|
|
|
|
r: 0.85,
|
|
|
|
|
|
g: 0.85,
|
|
|
|
|
|
b: 0.85,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
}),
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
})
|
|
|
|
|
|
.padding([4, 8])
|
|
|
|
|
|
.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
|
|
|
|
// ── Dynamic-input field formatting ─────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
/// Short prefix shown before a dynamic-input box's value.
|
|
|
|
|
|
fn dyn_component_label(c: DynComponent) -> String {
|
|
|
|
|
|
match c {
|
|
|
|
|
|
DynComponent::X => "X".into(),
|
|
|
|
|
|
DynComponent::Y => "Y".into(),
|
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 => "Z".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
|
|
|
|
DynComponent::Distance => "d".into(),
|
|
|
|
|
|
DynComponent::Angle => "<".into(),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 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).
|
|
|
|
|
|
fn dyn_component_value(
|
|
|
|
|
|
f: &DynFieldEntry,
|
|
|
|
|
|
w: glam::Vec3,
|
|
|
|
|
|
base: Option<glam::Vec3>,
|
|
|
|
|
|
) -> String {
|
|
|
|
|
|
if let Some(b) = &f.buffer {
|
|
|
|
|
|
return b.clone();
|
|
|
|
|
|
}
|
|
|
|
|
|
let b = base.unwrap_or(glam::Vec3::ZERO);
|
|
|
|
|
|
let dx = (w.x - b.x) as f64;
|
|
|
|
|
|
let dy = (w.y - b.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();
|
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 {
|
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::X if has_base => format!("{:.4}", dx),
|
|
|
|
|
|
DynComponent::Y if has_base => format!("{:.4}", dy),
|
|
|
|
|
|
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),
|
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::Distance => format!("{:.4}", (dx * dx + dy * dy).sqrt()),
|
|
|
|
|
|
DynComponent::Angle => format!("{:.1}", dy.atan2(dx).to_degrees().rem_euclid(360.0)),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|