forked from 1b-agent/opencadstudio-upstream
feat(model-space): theme-aware model space canvas and CAD selection/grip sysvars
Model space appearance now follows the active UI theme instead of a hardcoded dark background. Canvas mode is user-selectable (Match Theme / Classic CAD Dark / Custom RGB), with matching controls for the paper sheet and desk surround backgrounds and grid opacity. Implements industry-standard CAD system variables, all settable via command name, SETVAR, and the Options dialog: - COLORTHEME (0/1 dark/light UI scheme, preserves custom palettes) - COLORSCHEME (interactive named-theme selection) - BACKGROUND (THEME/CLASSIC/DEFAULT/DESK presets, RGB triplets) - SELECTIONAREA, SELECTIONAREAOPACITY (translucent marquee fill) - WINDOWSAREACOLOR/WINDOWAREACOLOR, CROSSINGAREACOLOR (ACI or theme) - SELECTIONEFFECT, SELECTIONEFFECTCOLOR (GPU highlight pass, driven by selection_generation invalidation — no retessellation) - SELECTIONPREVIEW (bit 0 = idle rollover, bit 1 = in-command hover) - GRIPSIZE, GRIPCOLOR, GRIPHOT, GRIPHOVER (grip overlay styling)
This commit is contained in:
1 parent
89b1a22850
commit
688785a6cf
18 files changed
+1949
-316
No files matched your search
@@ -427,6 +427,11 @@ impl OpenCADStudio {
|
|||||||
self.finish_active_command(&toks);
|
self.finish_active_command(&toks);
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
|
if tokens[0].eq_ignore_ascii_case("BACKGROUND")
|
||||||
|
|| tokens[0].eq_ignore_ascii_case("COLORSCHEME")
|
||||||
|
{
|
||||||
|
return self.dispatch_command(cmd);
|
||||||
|
}
|
||||||
let _ = self.dispatch_command(tokens[0]);
|
let _ = self.dispatch_command(tokens[0]);
|
||||||
if self.tabs[i].active_cmd.is_none() {
|
if self.tabs[i].active_cmd.is_none() {
|
||||||
// Not an interactive tool — an inline-argument command (`PDMODE 3`).
|
// Not an interactive tool — an inline-argument command (`PDMODE 3`).
|
||||||
|
|||||||
+73
-40
@@ -243,7 +243,7 @@ impl OpenCADStudio {
|
|||||||
use crate::command::ValuePromptCommand;
|
use crate::command::ValuePromptCommand;
|
||||||
let c = ValuePromptCommand::new(
|
let c = ValuePromptCommand::new(
|
||||||
"BACKGROUND",
|
"BACKGROUND",
|
||||||
"BACKGROUND colour [Default/Black/DarkGray/Gray/LightGray/White] or R G B (0–255):",
|
"BACKGROUND colour [Theme/Classic/Default/Black/DarkGray/Gray/LightGray/White] or R G B (0–255):",
|
||||||
);
|
);
|
||||||
self.command_line.push_info(&c.prompt());
|
self.command_line.push_info(&c.prompt());
|
||||||
self.tabs[i].active_cmd = Some(Box::new(c));
|
self.tabs[i].active_cmd = Some(Box::new(c));
|
||||||
@@ -251,56 +251,78 @@ impl OpenCADStudio {
|
|||||||
cmd if cmd.starts_with("BACKGROUND ") => {
|
cmd if cmd.starts_with("BACKGROUND ") => {
|
||||||
let args = cmd.split_whitespace().skip(1).collect::<Vec<_>>();
|
let args = cmd.split_whitespace().skip(1).collect::<Vec<_>>();
|
||||||
let is_paper = self.tabs[i].scene.current_layout != "Model";
|
let is_paper = self.tabs[i].scene.current_layout != "Model";
|
||||||
if args
|
let first_arg = args.first().map(|s| s.to_uppercase()).unwrap_or_default();
|
||||||
.first()
|
if first_arg == "THEME" || first_arg == "AUTO" {
|
||||||
.map(|s| s.eq_ignore_ascii_case("DEFAULT") || s.eq_ignore_ascii_case("RESET"))
|
self.model_space.mode = crate::app::config::ModelSpaceMode::MatchTheme;
|
||||||
.unwrap_or(false)
|
self.sync_model_space_theme(true);
|
||||||
{
|
self.persist_settings_if_changed();
|
||||||
|
self.command_line
|
||||||
|
.push_output(crate::t!("Model space background set to Match Theme.").as_ref());
|
||||||
|
} else if first_arg == "CLASSIC" {
|
||||||
|
self.model_space.mode = crate::app::config::ModelSpaceMode::ClassicDark;
|
||||||
|
self.sync_model_space_theme(true);
|
||||||
|
self.persist_settings_if_changed();
|
||||||
|
self.command_line
|
||||||
|
.push_output(crate::t!("Model space background set to Classic CAD Dark.").as_ref());
|
||||||
|
} else if first_arg == "DEFAULT" || first_arg == "RESET" {
|
||||||
if is_paper {
|
if is_paper {
|
||||||
self.tabs[i].paper_bg_color = None;
|
self.model_space.custom_paper_bg = None;
|
||||||
self.tabs[i].scene.paper_bg_color = [1.0, 1.0, 1.0, 1.0];
|
self.paper_bg_input.clear();
|
||||||
self.default_paper_bg_color = None;
|
|
||||||
} else {
|
} else {
|
||||||
self.tabs[i].bg_color = None;
|
self.model_space.mode = crate::app::config::ModelSpaceMode::MatchTheme;
|
||||||
self.tabs[i].scene.bg_color = [33.0 / 255.0, 40.0 / 255.0, 48.0 / 255.0, 1.0];
|
self.model_space.custom_bg = None;
|
||||||
self.default_bg_color = None;
|
self.model_bg_input.clear();
|
||||||
}
|
}
|
||||||
// Wire colour adaptation (`adapt_to_bg`) reads the bg
|
self.sync_model_space_theme(true);
|
||||||
// at tessellation time, so the cached wires need to
|
self.persist_settings_if_changed();
|
||||||
// refresh — otherwise a light→dark bg flip leaves
|
|
||||||
// black lines invisible against the new bg. Meshes
|
|
||||||
// bake colour into per-vertex GPU buffers at upload
|
|
||||||
// time; `recolor_meshes` rewrites the CPU side so
|
|
||||||
// the next epoch-driven re-upload picks up the new
|
|
||||||
// colour.
|
|
||||||
self.tabs[i].scene.recolor_meshes();
|
|
||||||
self.tabs[i].scene.bump_geometry();
|
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_output(crate::t!("Background reset to default.").as_ref());
|
.push_output(crate::t!("Background reset to default.").as_ref());
|
||||||
} else if let Some(rgba) = parse_background_color(&args) {
|
} else if first_arg == "DESK" {
|
||||||
if is_paper {
|
let sub_args = &args[1..];
|
||||||
self.tabs[i].paper_bg_color = Some(rgba);
|
let sub_first = sub_args.first().map(|s| s.to_uppercase()).unwrap_or_default();
|
||||||
self.tabs[i].scene.paper_bg_color = rgba;
|
if sub_first == "DEFAULT" || sub_first == "RESET" {
|
||||||
self.default_paper_bg_color = Some(rgba);
|
self.model_space.custom_desk_bg = None;
|
||||||
|
self.sync_model_space_theme(false);
|
||||||
|
self.persist_settings_if_changed();
|
||||||
|
self.command_line
|
||||||
|
.push_output(crate::t!("Paper desk surround background reset to default.").as_ref());
|
||||||
|
} else if let Some(rgba) = parse_background_color(sub_args) {
|
||||||
|
let [r, g, b, _] = rgba;
|
||||||
|
let rgb = [(r * 255.0).round() as u8, (g * 255.0).round() as u8, (b * 255.0).round() as u8];
|
||||||
|
self.model_space.custom_desk_bg = Some(rgb);
|
||||||
|
self.sync_model_space_theme(false);
|
||||||
|
self.persist_settings_if_changed();
|
||||||
|
self.command_line.push_output(crate::tf!(
|
||||||
|
"Desk surround background: rgb({}, {}, {})",
|
||||||
|
rgb[0],
|
||||||
|
rgb[1],
|
||||||
|
rgb[2]
|
||||||
|
).as_ref());
|
||||||
} else {
|
} else {
|
||||||
self.tabs[i].bg_color = Some(rgba);
|
self.command_line.push_info("Usage: BACKGROUND DESK <r> <g> <b> | DEFAULT");
|
||||||
self.tabs[i].scene.bg_color = rgba;
|
|
||||||
self.default_bg_color = Some(rgba);
|
|
||||||
}
|
}
|
||||||
self.tabs[i].scene.recolor_meshes();
|
} else if let Some(rgba) = parse_background_color(&args) {
|
||||||
self.tabs[i].scene.bump_geometry();
|
|
||||||
let [r, g, b, _] = rgba;
|
let [r, g, b, _] = rgba;
|
||||||
|
let rgb = [(r * 255.0).round() as u8, (g * 255.0).round() as u8, (b * 255.0).round() as u8];
|
||||||
|
if is_paper {
|
||||||
|
self.model_space.custom_paper_bg = Some(rgb);
|
||||||
|
self.paper_bg_input = crate::app::config::rgb_to_hex(rgb);
|
||||||
|
} else {
|
||||||
|
self.model_space.mode = crate::app::config::ModelSpaceMode::Custom;
|
||||||
|
self.model_space.custom_bg = Some(rgb);
|
||||||
|
self.model_bg_input = crate::app::config::rgb_to_hex(rgb);
|
||||||
|
}
|
||||||
|
self.sync_model_space_theme(true);
|
||||||
|
self.persist_settings_if_changed();
|
||||||
self.command_line.push_output(crate::tf!(
|
self.command_line.push_output(crate::tf!(
|
||||||
"Background: rgb({}, {}, {})",
|
"Background: rgb({}, {}, {})",
|
||||||
(r * 255.0).round() as u8,
|
rgb[0],
|
||||||
(g * 255.0).round() as u8,
|
rgb[1],
|
||||||
(b * 255.0).round() as u8
|
rgb[2]
|
||||||
).as_ref());
|
).as_ref());
|
||||||
// Persisted centrally after this message via
|
|
||||||
// `persist_settings_if_changed()`.
|
|
||||||
} else {
|
} else {
|
||||||
self.command_line.push_info(
|
self.command_line.push_info(
|
||||||
"Usage: BACKGROUND <r> <g> <b> (0–255) | DEFAULT|BLACK|DARKGRAY|GRAY|LIGHTGRAY|WHITE",
|
"Usage: BACKGROUND <r> <g> <b> (0–255) | THEME|CLASSIC|DEFAULT|DESK|BLACK|DARKGRAY|GRAY|LIGHTGRAY|WHITE",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -366,9 +388,20 @@ impl OpenCADStudio {
|
|||||||
/// Returns `None` if the arguments don't match either form.
|
/// Returns `None` if the arguments don't match either form.
|
||||||
fn parse_background_color(args: &[&str]) -> Option<[f32; 4]> {
|
fn parse_background_color(args: &[&str]) -> Option<[f32; 4]> {
|
||||||
let to_rgba = |[r, g, b]: [u8; 3]| [r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0];
|
let to_rgba = |[r, g, b]: [u8; 3]| [r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0];
|
||||||
// Single token: a named preset.
|
// Single token: a named preset or comma-separated "r,g,b".
|
||||||
if args.len() == 1 {
|
if args.len() == 1 {
|
||||||
let preset = match args[0].to_ascii_uppercase().as_str() {
|
let text = args[0].trim();
|
||||||
|
let parts: Vec<&str> = text.split(',').collect();
|
||||||
|
if parts.len() == 3 {
|
||||||
|
if let (Ok(r), Ok(g), Ok(b)) = (
|
||||||
|
parts[0].trim().parse::<u8>(),
|
||||||
|
parts[1].trim().parse::<u8>(),
|
||||||
|
parts[2].trim().parse::<u8>(),
|
||||||
|
) {
|
||||||
|
return Some(to_rgba([r, g, b]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let preset = match text.to_ascii_uppercase().as_str() {
|
||||||
"WHITE" => [255, 255, 255],
|
"WHITE" => [255, 255, 255],
|
||||||
"BLACK" => [0, 0, 0],
|
"BLACK" => [0, 0, 0],
|
||||||
"GRAY" | "GREY" => [128, 128, 128],
|
"GRAY" | "GREY" => [128, 128, 128],
|
||||||
|
|||||||
@@ -534,6 +534,19 @@ inventory::submit!(crate::command::CommandRegistration {
|
|||||||
"SKPOLY",
|
"SKPOLY",
|
||||||
"SKTOLERANCE",
|
"SKTOLERANCE",
|
||||||
"CLIPROMPTLINES",
|
"CLIPROMPTLINES",
|
||||||
|
"COLORTHEME",
|
||||||
|
"SELECTIONAREA",
|
||||||
|
"SELECTIONAREAOPACITY",
|
||||||
|
"SELECTIONEFFECT",
|
||||||
|
"SELECTIONEFFECTCOLOR",
|
||||||
|
"WINDOWSAREACOLOR",
|
||||||
|
"WINDOWAREACOLOR",
|
||||||
|
"CROSSINGAREACOLOR",
|
||||||
|
"SELECTIONPREVIEW",
|
||||||
|
"GRIPSIZE",
|
||||||
|
"GRIPCOLOR",
|
||||||
|
"GRIPHOT",
|
||||||
|
"GRIPHOVER",
|
||||||
// Reset selected entities' overrides to follow their layer.
|
// Reset selected entities' overrides to follow their layer.
|
||||||
"SETBYLAYER",
|
"SETBYLAYER",
|
||||||
// Remove duplicate objects; set drawing base point; audit integrity;
|
// Remove duplicate objects; set drawing base point; audit integrity;
|
||||||
|
|||||||
@@ -888,6 +888,19 @@ impl OpenCADStudio {
|
|||||||
| "CENTERCROSSGAP"
|
| "CENTERCROSSGAP"
|
||||||
| "CENTERMARKEXE"
|
| "CENTERMARKEXE"
|
||||||
| "DIMCONTINUEMODE"
|
| "DIMCONTINUEMODE"
|
||||||
|
| "COLORTHEME"
|
||||||
|
| "SELECTIONAREA"
|
||||||
|
| "SELECTIONAREAOPACITY"
|
||||||
|
| "SELECTIONEFFECT"
|
||||||
|
| "SELECTIONEFFECTCOLOR"
|
||||||
|
| "WINDOWSAREACOLOR"
|
||||||
|
| "WINDOWAREACOLOR"
|
||||||
|
| "CROSSINGAREACOLOR"
|
||||||
|
| "SELECTIONPREVIEW"
|
||||||
|
| "GRIPSIZE"
|
||||||
|
| "GRIPCOLOR"
|
||||||
|
| "GRIPHOT"
|
||||||
|
| "GRIPHOVER"
|
||||||
) =>
|
) =>
|
||||||
{
|
{
|
||||||
return self.dispatch_styleprops(&format!("SETVAR {cmd}"), i);
|
return self.dispatch_styleprops(&format!("SETVAR {cmd}"), i);
|
||||||
@@ -910,7 +923,7 @@ impl OpenCADStudio {
|
|||||||
let value = it.next().map(|s| s.trim().to_string());
|
let value = it.next().map(|s| s.trim().to_string());
|
||||||
if name.is_empty() || name == "?" {
|
if name.is_empty() || name == "?" {
|
||||||
self.command_line.push_info(
|
self.command_line.push_info(
|
||||||
"SETVAR: LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE | CLAYER CELTYPE TEXTSTYLE (read-only)",
|
"SETVAR: LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER | CLAYER CELTYPE TEXTSTYLE (read-only)",
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
let frame_kind = crate::scene::frame::kind_for_name(&name);
|
let frame_kind = crate::scene::frame::kind_for_name(&name);
|
||||||
@@ -1251,6 +1264,171 @@ impl OpenCADStudio {
|
|||||||
false,
|
false,
|
||||||
)),
|
)),
|
||||||
},
|
},
|
||||||
|
"COLORTHEME" => match &value {
|
||||||
|
Some(v) => match v.as_str() {
|
||||||
|
"0" => {
|
||||||
|
if self.ui_theme.name == "Custom" {
|
||||||
|
self.saved_custom_palette = Some(self.ui_theme.palette);
|
||||||
|
}
|
||||||
|
self.ui_theme.name = iced::Theme::Dark.to_string();
|
||||||
|
self.ui_theme.palette =
|
||||||
|
crate::app::config::UiThemePalette::from_iced(iced::Theme::Dark.seed());
|
||||||
|
self.theme_color_inputs = self.ui_theme.palette.hex_values();
|
||||||
|
self.active_theme = iced::Theme::Dark;
|
||||||
|
self.sync_model_space_theme(true);
|
||||||
|
Ok(("COLORTHEME = 0 (Dark)".to_string(), true))
|
||||||
|
}
|
||||||
|
"1" => {
|
||||||
|
if self.ui_theme.name == "Custom" {
|
||||||
|
self.saved_custom_palette = Some(self.ui_theme.palette);
|
||||||
|
}
|
||||||
|
self.ui_theme.name = iced::Theme::Light.to_string();
|
||||||
|
self.ui_theme.palette =
|
||||||
|
crate::app::config::UiThemePalette::from_iced(iced::Theme::Light.seed());
|
||||||
|
self.theme_color_inputs = self.ui_theme.palette.hex_values();
|
||||||
|
self.active_theme = iced::Theme::Light;
|
||||||
|
self.sync_model_space_theme(true);
|
||||||
|
Ok(("COLORTHEME = 1 (Light)".to_string(), true))
|
||||||
|
}
|
||||||
|
_ => Err("SETVAR: 0 (Dark) or 1 (Light) required.".into()),
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
let bg = self.ui_theme.palette.background;
|
||||||
|
let lum = 0.299 * (bg[0] as f32) + 0.587 * (bg[1] as f32) + 0.114 * (bg[2] as f32);
|
||||||
|
let code = if lum > 128.0 { 1 } else { 0 };
|
||||||
|
Ok((format!("COLORTHEME = {code} ({})", self.ui_theme.name), false))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"SELECTIONAREA" => match &value {
|
||||||
|
Some(v) => match v.as_str() {
|
||||||
|
"0" => {
|
||||||
|
self.model_space.selection_area = false;
|
||||||
|
self.sync_model_space_theme(false);
|
||||||
|
Ok(("SELECTIONAREA = 0".to_string(), true))
|
||||||
|
}
|
||||||
|
"1" => {
|
||||||
|
self.model_space.selection_area = true;
|
||||||
|
self.sync_model_space_theme(false);
|
||||||
|
Ok(("SELECTIONAREA = 1".to_string(), true))
|
||||||
|
}
|
||||||
|
_ => Err("SETVAR: 0 or 1 required.".into()),
|
||||||
|
},
|
||||||
|
None => Ok((format!("SELECTIONAREA = {}", if self.model_space.selection_area { 1 } else { 0 }), false)),
|
||||||
|
},
|
||||||
|
"SELECTIONAREAOPACITY" => match &value {
|
||||||
|
Some(v) => match v.parse::<u8>() {
|
||||||
|
Ok(op) if op <= 100 => {
|
||||||
|
self.model_space.selection_opacity = op;
|
||||||
|
self.sync_model_space_theme(false);
|
||||||
|
Ok((format!("SELECTIONAREAOPACITY = {op}"), true))
|
||||||
|
}
|
||||||
|
_ => Err("SETVAR: integer from 0 to 100 required.".into()),
|
||||||
|
},
|
||||||
|
None => Ok((format!("SELECTIONAREAOPACITY = {}", self.model_space.selection_opacity), false)),
|
||||||
|
},
|
||||||
|
"SELECTIONEFFECT" => match &value {
|
||||||
|
Some(v) => match v.as_str() {
|
||||||
|
"0" => {
|
||||||
|
self.model_space.selection_effect = false;
|
||||||
|
self.sync_model_space_theme(false);
|
||||||
|
Ok(("SELECTIONEFFECT = 0".to_string(), true))
|
||||||
|
}
|
||||||
|
"1" => {
|
||||||
|
self.model_space.selection_effect = true;
|
||||||
|
self.sync_model_space_theme(false);
|
||||||
|
Ok(("SELECTIONEFFECT = 1".to_string(), true))
|
||||||
|
}
|
||||||
|
_ => Err("SETVAR: 0 or 1 required.".into()),
|
||||||
|
},
|
||||||
|
None => Ok((format!("SELECTIONEFFECT = {}", if self.model_space.selection_effect { 1 } else { 0 }), false)),
|
||||||
|
},
|
||||||
|
"SELECTIONEFFECTCOLOR" => match &value {
|
||||||
|
Some(v) => match v.parse::<u8>() {
|
||||||
|
Ok(color) => {
|
||||||
|
self.model_space.selection_highlight_color = color;
|
||||||
|
self.sync_model_space_theme(false);
|
||||||
|
Ok((format!("SELECTIONEFFECTCOLOR = {color}"), true))
|
||||||
|
}
|
||||||
|
_ => Err("SETVAR: integer from 0 (Theme) to 255 required.".into()),
|
||||||
|
},
|
||||||
|
None => Ok((format!("SELECTIONEFFECTCOLOR = {}", self.model_space.selection_highlight_color), false)),
|
||||||
|
},
|
||||||
|
"WINDOWSAREACOLOR" | "WINDOWAREACOLOR" => match &value {
|
||||||
|
Some(v) => match v.parse::<u8>() {
|
||||||
|
Ok(color) => {
|
||||||
|
self.model_space.selection_window_color = color;
|
||||||
|
self.sync_model_space_theme(false);
|
||||||
|
Ok((format!("WINDOWSAREACOLOR = {color}"), true))
|
||||||
|
}
|
||||||
|
_ => Err("SETVAR: integer from 0 (Theme) to 255 required.".into()),
|
||||||
|
},
|
||||||
|
None => Ok((format!("WINDOWSAREACOLOR = {}", self.model_space.selection_window_color), false)),
|
||||||
|
},
|
||||||
|
"CROSSINGAREACOLOR" => match &value {
|
||||||
|
Some(v) => match v.parse::<u8>() {
|
||||||
|
Ok(color) => {
|
||||||
|
self.model_space.selection_crossing_color = color;
|
||||||
|
self.sync_model_space_theme(false);
|
||||||
|
Ok((format!("CROSSINGAREACOLOR = {color}"), true))
|
||||||
|
}
|
||||||
|
_ => Err("SETVAR: integer from 0 (Theme) to 255 required.".into()),
|
||||||
|
},
|
||||||
|
None => Ok((format!("CROSSINGAREACOLOR = {}", self.model_space.selection_crossing_color), false)),
|
||||||
|
},
|
||||||
|
"SELECTIONPREVIEW" => match &value {
|
||||||
|
Some(v) => match v.parse::<u8>() {
|
||||||
|
Ok(mode @ 0..=3) => {
|
||||||
|
self.model_space.selection_preview = mode;
|
||||||
|
Ok((format!("SELECTIONPREVIEW = {mode}"), true))
|
||||||
|
}
|
||||||
|
_ => Err("SETVAR: integer from 0 to 3 required.".into()),
|
||||||
|
},
|
||||||
|
None => Ok((format!("SELECTIONPREVIEW = {}", self.model_space.selection_preview), false)),
|
||||||
|
},
|
||||||
|
"GRIPSIZE" => match &value {
|
||||||
|
Some(v) => match v.parse::<u8>() {
|
||||||
|
Ok(size @ 1..=25) => {
|
||||||
|
self.model_space.grip_size = size;
|
||||||
|
self.sync_model_space_theme(false);
|
||||||
|
Ok((format!("GRIPSIZE = {size}"), true))
|
||||||
|
}
|
||||||
|
_ => Err("SETVAR: integer from 1 to 25 required.".into()),
|
||||||
|
},
|
||||||
|
None => Ok((format!("GRIPSIZE = {}", self.model_space.grip_size), false)),
|
||||||
|
},
|
||||||
|
"GRIPCOLOR" => match &value {
|
||||||
|
Some(v) => match v.parse::<u8>() {
|
||||||
|
Ok(color) => {
|
||||||
|
self.model_space.grip_color = color;
|
||||||
|
self.sync_model_space_theme(false);
|
||||||
|
Ok((format!("GRIPCOLOR = {color}"), true))
|
||||||
|
}
|
||||||
|
_ => Err("SETVAR: integer from 0 (Theme) to 255 required.".into()),
|
||||||
|
},
|
||||||
|
None => Ok((format!("GRIPCOLOR = {}", self.model_space.grip_color), false)),
|
||||||
|
},
|
||||||
|
"GRIPHOT" => match &value {
|
||||||
|
Some(v) => match v.parse::<u8>() {
|
||||||
|
Ok(color) => {
|
||||||
|
self.model_space.grip_hot = color;
|
||||||
|
self.sync_model_space_theme(false);
|
||||||
|
Ok((format!("GRIPHOT = {color}"), true))
|
||||||
|
}
|
||||||
|
_ => Err("SETVAR: integer from 0 (Theme) to 255 required.".into()),
|
||||||
|
},
|
||||||
|
None => Ok((format!("GRIPHOT = {}", self.model_space.grip_hot), false)),
|
||||||
|
},
|
||||||
|
"GRIPHOVER" => match &value {
|
||||||
|
Some(v) => match v.parse::<u8>() {
|
||||||
|
Ok(color) => {
|
||||||
|
self.model_space.grip_hover = color;
|
||||||
|
self.sync_model_space_theme(false);
|
||||||
|
Ok((format!("GRIPHOVER = {color}"), true))
|
||||||
|
}
|
||||||
|
_ => Err("SETVAR: integer from 0 (Theme) to 255 required.".into()),
|
||||||
|
},
|
||||||
|
None => Ok((format!("GRIPHOVER = {}", self.model_space.grip_hover), false)),
|
||||||
|
},
|
||||||
"SNAPANG" => match &value {
|
"SNAPANG" => match &value {
|
||||||
Some(v) => match v.parse::<f32>() {
|
Some(v) => match v.parse::<f32>() {
|
||||||
Ok(angle) if angle.is_finite() => {
|
Ok(angle) if angle.is_finite() => {
|
||||||
@@ -1872,6 +2050,19 @@ impl OpenCADStudio {
|
|||||||
| "CURSORTYPE"
|
| "CURSORTYPE"
|
||||||
| "SNAPANG"
|
| "SNAPANG"
|
||||||
| "CLIPROMPTLINES"
|
| "CLIPROMPTLINES"
|
||||||
|
| "COLORTHEME"
|
||||||
|
| "SELECTIONAREA"
|
||||||
|
| "SELECTIONAREAOPACITY"
|
||||||
|
| "SELECTIONEFFECT"
|
||||||
|
| "SELECTIONEFFECTCOLOR"
|
||||||
|
| "WINDOWSAREACOLOR"
|
||||||
|
| "WINDOWAREACOLOR"
|
||||||
|
| "CROSSINGAREACOLOR"
|
||||||
|
| "SELECTIONPREVIEW"
|
||||||
|
| "GRIPSIZE"
|
||||||
|
| "GRIPCOLOR"
|
||||||
|
| "GRIPHOT"
|
||||||
|
| "GRIPHOVER"
|
||||||
) {
|
) {
|
||||||
self.persist_settings_if_changed();
|
self.persist_settings_if_changed();
|
||||||
} else {
|
} else {
|
||||||
@@ -2671,3 +2862,230 @@ fn rename_symbol(doc: &mut acadrust::CadDocument, ty: &str, old: &str, new: &str
|
|||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::app::OpenCADStudio;
|
||||||
|
|
||||||
|
fn fresh_app() -> OpenCADStudio {
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
app.automation_op(r#"{"op":"new"}"#);
|
||||||
|
app
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cad_selection_and_model_space_sysvars() {
|
||||||
|
let mut app = fresh_app();
|
||||||
|
|
||||||
|
// COLORTHEME 1 (Light)
|
||||||
|
let _ = app.run_command_line("SETVAR COLORTHEME 1");
|
||||||
|
assert_eq!(app.active_theme, iced::Theme::Light);
|
||||||
|
assert_eq!(app.ui_theme.name, "Light");
|
||||||
|
|
||||||
|
// COLORTHEME 0 (Dark)
|
||||||
|
let _ = app.run_command_line("SETVAR COLORTHEME 0");
|
||||||
|
assert_eq!(app.active_theme, iced::Theme::Dark);
|
||||||
|
assert_eq!(app.ui_theme.name, "Dark");
|
||||||
|
|
||||||
|
// Direct command name: COLORTHEME 1
|
||||||
|
let _ = app.run_command_line("COLORTHEME 1");
|
||||||
|
assert_eq!(app.active_theme, iced::Theme::Light);
|
||||||
|
|
||||||
|
// SELECTIONAREA toggle
|
||||||
|
let _ = app.run_command_line("SETVAR SELECTIONAREA 0");
|
||||||
|
assert!(!app.model_space.selection_area);
|
||||||
|
let _ = app.run_command_line("SELECTIONAREA 1");
|
||||||
|
assert!(app.model_space.selection_area);
|
||||||
|
|
||||||
|
// SELECTIONAREAOPACITY
|
||||||
|
let _ = app.run_command_line("SETVAR SELECTIONAREAOPACITY 45");
|
||||||
|
assert_eq!(app.model_space.selection_opacity, 45);
|
||||||
|
let _ = app.run_command_line("SELECTIONAREAOPACITY 80");
|
||||||
|
assert_eq!(app.model_space.selection_opacity, 80);
|
||||||
|
|
||||||
|
// SELECTIONEFFECT
|
||||||
|
let _ = app.run_command_line("SETVAR SELECTIONEFFECT 0");
|
||||||
|
assert!(!app.model_space.selection_effect);
|
||||||
|
assert!(!app.tabs[app.active_tab].scene.selection_effect);
|
||||||
|
let _ = app.run_command_line("SELECTIONEFFECT 1");
|
||||||
|
assert!(app.model_space.selection_effect);
|
||||||
|
assert!(app.tabs[app.active_tab].scene.selection_effect);
|
||||||
|
|
||||||
|
// SELECTIONEFFECTCOLOR
|
||||||
|
let _ = app.run_command_line("SETVAR SELECTIONEFFECTCOLOR 4");
|
||||||
|
assert_eq!(app.model_space.selection_highlight_color, 4);
|
||||||
|
assert_eq!(
|
||||||
|
app.tabs[app.active_tab].scene.selection_color,
|
||||||
|
app.model_space.resolve_selection_color()
|
||||||
|
);
|
||||||
|
|
||||||
|
// WINDOWSAREACOLOR & CROSSINGAREACOLOR
|
||||||
|
let _ = app.run_command_line("SETVAR WINDOWSAREACOLOR 5");
|
||||||
|
assert_eq!(app.model_space.selection_window_color, 5);
|
||||||
|
let _ = app.run_command_line("WINDOWSAREACOLOR 1");
|
||||||
|
assert_eq!(app.model_space.selection_window_color, 1);
|
||||||
|
|
||||||
|
let _ = app.run_command_line("SETVAR CROSSINGAREACOLOR 3");
|
||||||
|
assert_eq!(app.model_space.selection_crossing_color, 3);
|
||||||
|
let _ = app.run_command_line("CROSSINGAREACOLOR 2");
|
||||||
|
assert_eq!(app.model_space.selection_crossing_color, 2);
|
||||||
|
|
||||||
|
// SELECTIONPREVIEW
|
||||||
|
let _ = app.run_command_line("SETVAR SELECTIONPREVIEW 2");
|
||||||
|
assert_eq!(app.model_space.selection_preview, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_background_command_modes() {
|
||||||
|
let mut app = fresh_app();
|
||||||
|
let i = app.active_tab;
|
||||||
|
|
||||||
|
// BACKGROUND CLASSIC
|
||||||
|
let _ = app.run_command_line("BACKGROUND CLASSIC");
|
||||||
|
assert_eq!(app.model_space.mode, crate::app::config::ModelSpaceMode::ClassicDark);
|
||||||
|
assert_eq!(
|
||||||
|
app.tabs[i].scene.bg_color,
|
||||||
|
[33.0 / 255.0, 40.0 / 255.0, 48.0 / 255.0, 1.0]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Switch to light theme and set BACKGROUND THEME
|
||||||
|
let _ = app.run_command_line("COLORTHEME 1");
|
||||||
|
let _ = app.run_command_line("BACKGROUND THEME");
|
||||||
|
assert_eq!(app.model_space.mode, crate::app::config::ModelSpaceMode::MatchTheme);
|
||||||
|
assert_eq!(app.tabs[i].scene.bg_color, [1.0, 1.0, 1.0, 1.0]);
|
||||||
|
|
||||||
|
// BACKGROUND RGB custom
|
||||||
|
let _ = app.run_command_line("BACKGROUND 50 60 70");
|
||||||
|
assert_eq!(app.model_space.mode, crate::app::config::ModelSpaceMode::Custom);
|
||||||
|
assert_eq!(app.model_space.custom_bg, Some([50, 60, 70]));
|
||||||
|
assert!((app.tabs[i].scene.bg_color[0] - 50.0 / 255.0).abs() < 1e-4);
|
||||||
|
assert!((app.tabs[i].scene.bg_color[1] - 60.0 / 255.0).abs() < 1e-4);
|
||||||
|
assert!((app.tabs[i].scene.bg_color[2] - 70.0 / 255.0).abs() < 1e-4);
|
||||||
|
|
||||||
|
// BACKGROUND DEFAULT restores MatchTheme
|
||||||
|
let _ = app.run_command_line("BACKGROUND DEFAULT");
|
||||||
|
assert_eq!(app.model_space.mode, crate::app::config::ModelSpaceMode::MatchTheme);
|
||||||
|
assert_eq!(app.model_space.custom_bg, None);
|
||||||
|
|
||||||
|
// BACKGROUND DESK
|
||||||
|
let _ = app.run_command_line("BACKGROUND DESK 80 85 90");
|
||||||
|
assert_eq!(app.model_space.custom_desk_bg, Some([80, 85, 90]));
|
||||||
|
let _ = app.run_command_line("BACKGROUND DESK DEFAULT");
|
||||||
|
assert_eq!(app.model_space.custom_desk_bg, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_colorscheme_command() {
|
||||||
|
let mut app = fresh_app();
|
||||||
|
app.model_space.mode = crate::app::config::ModelSpaceMode::MatchTheme;
|
||||||
|
let i = app.active_tab;
|
||||||
|
|
||||||
|
// Bare COLORSCHEME launches interactive ValuePromptCommand
|
||||||
|
let _ = app.run_command_line("COLORSCHEME");
|
||||||
|
assert!(app.tabs[i].active_cmd.is_some());
|
||||||
|
assert_eq!(app.tabs[i].active_cmd.as_ref().unwrap().name(), "COLORSCHEME");
|
||||||
|
|
||||||
|
// Cancel the interactive command
|
||||||
|
app.tabs[i].active_cmd = None;
|
||||||
|
|
||||||
|
// COLORSCHEME LIGHT sets Light theme and synchronizes model space
|
||||||
|
let _ = app.run_command_line("COLORSCHEME LIGHT");
|
||||||
|
assert_eq!(app.active_theme, iced::Theme::Light);
|
||||||
|
assert_eq!(app.tabs[i].scene.bg_color, [1.0, 1.0, 1.0, 1.0]);
|
||||||
|
|
||||||
|
// Case-insensitive and spaces: "colorscheme tokyo night"
|
||||||
|
let _ = app.run_command_line("colorscheme tokyo night");
|
||||||
|
assert_eq!(app.active_theme, iced::Theme::TokyoNight);
|
||||||
|
|
||||||
|
// COLORSCHEME rejects 1 and 0 (which belong to COLORTHEME)
|
||||||
|
let _ = app.run_command_line("colorscheme 1");
|
||||||
|
assert_ne!(app.active_theme, iced::Theme::Light); // stays TokyoNight
|
||||||
|
|
||||||
|
// COLORTHEME accepts 1 and 0
|
||||||
|
let _ = app.run_command_line("COLORTHEME 1");
|
||||||
|
assert_eq!(app.active_theme, iced::Theme::Light);
|
||||||
|
let _ = app.run_command_line("COLORTHEME 0");
|
||||||
|
assert_eq!(app.active_theme, iced::Theme::Dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_reviewer_feedback_fixes() {
|
||||||
|
let mut app = fresh_app();
|
||||||
|
|
||||||
|
// 1. COLORTHEME query returns UI theme scheme regardless of canvas background
|
||||||
|
let _ = app.run_command_line("COLORTHEME 0");
|
||||||
|
// Set canvas background to bright white in custom mode
|
||||||
|
let _ = app.run_command_line("BACKGROUND 255 255 255");
|
||||||
|
let _ = app.run_command_line("SETVAR COLORTHEME");
|
||||||
|
let output = app.command_line.history_plain_text();
|
||||||
|
assert!(output.contains("<0 (Dark)>"), "UI is dark, so query must return 0 despite white canvas: {output}");
|
||||||
|
|
||||||
|
let _ = app.run_command_line("COLORTHEME 1");
|
||||||
|
let _ = app.run_command_line("SETVAR COLORTHEME");
|
||||||
|
let output = app.command_line.history_plain_text();
|
||||||
|
assert!(output.contains("<1 (Light)>"), "UI is light, so query must return 1: {output}");
|
||||||
|
|
||||||
|
// 2. Custom theme safety when COLORTHEME 0/1 or COLORSCHEME runs
|
||||||
|
app.ui_theme.name = "Custom".to_string();
|
||||||
|
app.ui_theme.palette.background = [12, 34, 56];
|
||||||
|
let _ = app.run_command_line("COLORTHEME 0");
|
||||||
|
assert_eq!(app.saved_custom_palette.map(|p| p.background), Some([12, 34, 56]));
|
||||||
|
|
||||||
|
app.saved_custom_palette = None;
|
||||||
|
app.ui_theme.name = "Custom".to_string();
|
||||||
|
app.ui_theme.palette.background = [78, 90, 12];
|
||||||
|
let _ = app.run_command_line("COLORSCHEME DRACULA");
|
||||||
|
assert_eq!(app.saved_custom_palette.map(|p| p.background), Some([78, 90, 12]));
|
||||||
|
|
||||||
|
// 3. ModelSpaceBgChanged empty reverts mode to MatchTheme
|
||||||
|
let _ = app.update(crate::app::Message::ModelSpaceBgChanged("".to_string()));
|
||||||
|
assert_eq!(app.model_space.mode, crate::app::config::ModelSpaceMode::MatchTheme);
|
||||||
|
assert_eq!(app.model_space.custom_bg, None);
|
||||||
|
|
||||||
|
// 4. Split restore defaults: Display vs Selection
|
||||||
|
app.model_space.mode = crate::app::config::ModelSpaceMode::Custom;
|
||||||
|
app.model_space.custom_bg = Some([10, 20, 30]);
|
||||||
|
app.model_space.selection_opacity = 99;
|
||||||
|
let _ = app.update(crate::app::Message::RestoreModelSpaceDisplayDefaults);
|
||||||
|
assert_eq!(app.model_space.mode, crate::app::config::ModelSpaceMode::MatchTheme);
|
||||||
|
assert_eq!(app.model_space.selection_opacity, 99, "Selection visual must not be wiped by display restore");
|
||||||
|
|
||||||
|
let _ = app.update(crate::app::Message::RestoreSelectionVisualDefaults);
|
||||||
|
assert_eq!(app.model_space.selection_opacity, 12, "Selection restore resets selection visual defaults");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_grip_and_desk_surround_sysvars() {
|
||||||
|
let mut app = fresh_app();
|
||||||
|
|
||||||
|
// 1. Desk surround background message
|
||||||
|
let _ = app.update(crate::app::Message::DeskSpaceBgChanged("#1a2b3c".to_string()));
|
||||||
|
assert_eq!(app.model_space.custom_desk_bg, Some([0x1a, 0x2b, 0x3c]));
|
||||||
|
assert_eq!(app.desk_bg_input, "#1a2b3c");
|
||||||
|
let _ = app.update(crate::app::Message::DeskSpaceBgChanged("".to_string()));
|
||||||
|
assert_eq!(app.model_space.custom_desk_bg, None);
|
||||||
|
|
||||||
|
// 2. GRIPSIZE, GRIPCOLOR, GRIPHOT, GRIPHOVER via command line & SETVAR
|
||||||
|
let _ = app.run_command_line("GRIPSIZE 8");
|
||||||
|
assert_eq!(app.model_space.grip_size, 8);
|
||||||
|
let _ = app.run_command_line("GRIPCOLOR 4");
|
||||||
|
assert_eq!(app.model_space.grip_color, 4);
|
||||||
|
let _ = app.run_command_line("GRIPHOT 1");
|
||||||
|
assert_eq!(app.model_space.grip_hot, 1);
|
||||||
|
let _ = app.run_command_line("GRIPHOVER 2");
|
||||||
|
assert_eq!(app.model_space.grip_hover, 2);
|
||||||
|
|
||||||
|
// Direct messages
|
||||||
|
let _ = app.update(crate::app::Message::GripSizeChanged(12));
|
||||||
|
assert_eq!(app.model_space.grip_size, 12);
|
||||||
|
let _ = app.update(crate::app::Message::GripColorChanged(5));
|
||||||
|
assert_eq!(app.model_space.grip_color, 5);
|
||||||
|
|
||||||
|
// Restore defaults resets grips
|
||||||
|
let _ = app.update(crate::app::Message::RestoreSelectionVisualDefaults);
|
||||||
|
assert_eq!(app.model_space.grip_size, 5);
|
||||||
|
assert_eq!(app.model_space.grip_color, 0);
|
||||||
|
assert_eq!(app.model_space.grip_hot, 0);
|
||||||
|
assert_eq!(app.model_space.grip_hover, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
+61
-45
@@ -375,56 +375,72 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Color Scheme / Theme selector ─────────────────────────────
|
// ── Color Scheme / Theme selector ─────────────────────────────
|
||||||
cmd if cmd == "COLORSCHEME" || cmd.starts_with("COLORSCHEME ") => {
|
cmd if cmd.eq_ignore_ascii_case("COLORSCHEME") => {
|
||||||
use iced::Theme;
|
use crate::command::ValuePromptCommand;
|
||||||
let sub = cmd
|
let c = ValuePromptCommand::new(
|
||||||
.split_once(' ')
|
"COLORSCHEME",
|
||||||
.map(|(_, r)| r.trim())
|
"COLORSCHEME Enter theme name (or ? to list):",
|
||||||
.unwrap_or("")
|
);
|
||||||
.to_uppercase();
|
self.command_line.push_output(
|
||||||
// Map name to Theme variant.
|
"Available themes: DARK LIGHT DRACULA NORD SOLARIZED_LIGHT SOLARIZED_DARK \
|
||||||
let theme: Option<Theme> = match sub.as_str() {
|
GRUVBOX_LIGHT GRUVBOX_DARK TOKYONIGHT TOKYONIGHTSTORM TOKYONIGHTLIGHT \
|
||||||
"DARK" => Some(Theme::Dark),
|
KANAGAWAWAVE KANAGAWADRAGON KANAGAWALOTUS MOONFLY NIGHTFLY OXOCARBON FERRA",
|
||||||
"LIGHT" => Some(Theme::Light),
|
);
|
||||||
"DRACULA" => Some(Theme::Dracula),
|
self.command_line.push_info(&format!(
|
||||||
"NORD" => Some(Theme::Nord),
|
"Current color scheme: <{}>",
|
||||||
"SOLARIZED_LIGHT" | "SOLARIZEDLIGHT" => Some(Theme::SolarizedLight),
|
self.ui_theme.name
|
||||||
"SOLARIZED_DARK" | "SOLARIZEDDARK" => Some(Theme::SolarizedDark),
|
));
|
||||||
"GRUVBOX_LIGHT" | "GRUVBOXLIGHT" => Some(Theme::GruvboxLight),
|
self.command_line.push_info(&c.prompt());
|
||||||
"GRUVBOX_DARK" | "GRUVBOXDARK" => Some(Theme::GruvboxDark),
|
self.tabs[i].active_cmd = Some(Box::new(c));
|
||||||
"TOKYONIGHT" | "TOKYO_NIGHT" => Some(Theme::TokyoNight),
|
return Some(Task::none());
|
||||||
"TOKYONIGHTSTORM" | "TOKYO_NIGHT_STORM" => Some(Theme::TokyoNightStorm),
|
}
|
||||||
"TOKYONIGHTLIGHT" | "TOKYO_NIGHT_LIGHT" => Some(Theme::TokyoNightLight),
|
cmd if cmd.to_ascii_uppercase().starts_with("COLORSCHEME ") => {
|
||||||
"KANAGAWAWAVE" | "KANAGAWA_WAVE" => Some(Theme::KanagawaWave),
|
let sub = cmd.split_once(' ').map(|(_, r)| r.trim()).unwrap_or("");
|
||||||
"KANAGAWADRAGON" | "KANAGAWA_DRAGON" => Some(Theme::KanagawaDragon),
|
let upper_sub = sub.to_ascii_uppercase();
|
||||||
"KANAGAWALOTUS" | "KANAGAWA_LOTUS" => Some(Theme::KanagawaLotus),
|
if upper_sub.is_empty() {
|
||||||
"MOONFLY" => Some(Theme::Moonfly),
|
self.command_line.push_output(&format!(
|
||||||
"NIGHTFLY" => Some(Theme::Nightfly),
|
"Current color scheme: <{}>",
|
||||||
"OXOCARBON" => Some(Theme::Oxocarbon),
|
self.ui_theme.name
|
||||||
"FERRA" => Some(Theme::Ferra),
|
));
|
||||||
"" | "LIST" | "?" => {
|
return Some(Task::none());
|
||||||
self.command_line.push_output(
|
}
|
||||||
"Available themes: DARK LIGHT DRACULA NORD SOLARIZED_LIGHT SOLARIZED_DARK \
|
if upper_sub == "LIST" || upper_sub == "?" {
|
||||||
GRUVBOX_LIGHT GRUVBOX_DARK TOKYONIGHT TOKYONIGHTSTORM TOKYONIGHTLIGHT \
|
self.command_line.push_output(
|
||||||
KANAGAWAWAVE KANAGAWADRAGON KANAGAWALOTUS MOONFLY NIGHTFLY OXOCARBON FERRA"
|
"Available themes: DARK LIGHT DRACULA NORD SOLARIZED_LIGHT SOLARIZED_DARK \
|
||||||
);
|
GRUVBOX_LIGHT GRUVBOX_DARK TOKYONIGHT TOKYONIGHTSTORM TOKYONIGHTLIGHT \
|
||||||
return Some(Task::none());
|
KANAGAWAWAVE KANAGAWADRAGON KANAGAWALOTUS MOONFLY NIGHTFLY OXOCARBON FERRA",
|
||||||
}
|
);
|
||||||
_ => {
|
return Some(Task::none());
|
||||||
self.command_line.push_error(crate::tf!(
|
}
|
||||||
"COLORSCHEME: unknown theme '{}'. Type COLORSCHEME LIST for options.",
|
if sub == "0" || sub == "1" {
|
||||||
sub
|
self.command_line.push_error(
|
||||||
).as_ref());
|
"COLORSCHEME: 0 and 1 are COLORTHEME options. Enter a theme name (e.g. DARK, LIGHT, DRACULA) or use COLORTHEME.",
|
||||||
return Some(Task::none());
|
);
|
||||||
}
|
return Some(Task::none());
|
||||||
};
|
}
|
||||||
|
let theme = crate::app::config::parse_theme_name(sub);
|
||||||
if let Some(t) = theme {
|
if let Some(t) = theme {
|
||||||
|
if self.ui_theme.name == "Custom" {
|
||||||
|
self.saved_custom_palette = Some(self.ui_theme.palette);
|
||||||
|
}
|
||||||
let name = format!("{:?}", t);
|
let name = format!("{:?}", t);
|
||||||
|
self.ui_theme.name = t.to_string();
|
||||||
|
self.ui_theme.palette =
|
||||||
|
crate::app::config::UiThemePalette::from_iced(t.seed());
|
||||||
|
self.theme_color_inputs = self.ui_theme.palette.hex_values();
|
||||||
|
self.active_theme = t.clone();
|
||||||
|
self.sync_model_space_theme(true);
|
||||||
|
self.persist_settings_if_changed();
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_output(crate::tf!("Color scheme set to '{name}'.").as_ref());
|
.push_output(crate::tf!("Color scheme set to '{name}'.").as_ref());
|
||||||
return Some(Task::done(Message::SetTheme(t)));
|
return Some(Task::none());
|
||||||
|
} else {
|
||||||
|
self.command_line.push_error(crate::tf!(
|
||||||
|
"COLORSCHEME: unknown theme '{}'. Type COLORSCHEME LIST for options.",
|
||||||
|
sub
|
||||||
|
).as_ref());
|
||||||
|
return Some(Task::none());
|
||||||
}
|
}
|
||||||
return Some(Task::none());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Layout Manager GUI ─────────────────────────────────────────
|
// ── Layout Manager GUI ─────────────────────────────────────────
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ pub struct AppConfig {
|
|||||||
pub plot: PlotDialogState,
|
pub plot: PlotDialogState,
|
||||||
/// Complete editable keyboard shortcut table.
|
/// Complete editable keyboard shortcut table.
|
||||||
pub shortcuts: ShortcutConfig,
|
pub shortcuts: ShortcutConfig,
|
||||||
|
/// Model space background, grid, and selection appearance.
|
||||||
|
pub model_space: ModelSpaceThemeConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for AppConfig {
|
impl Default for AppConfig {
|
||||||
@@ -54,6 +56,7 @@ impl Default for AppConfig {
|
|||||||
ribbon: RibbonConfig::default(),
|
ribbon: RibbonConfig::default(),
|
||||||
plot: PlotDialogState::default(),
|
plot: PlotDialogState::default(),
|
||||||
shortcuts: ShortcutConfig::default(),
|
shortcuts: ShortcutConfig::default(),
|
||||||
|
model_space: ModelSpaceThemeConfig::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -208,6 +211,206 @@ pub(crate) fn parse_hex(value: &str) -> Option<[u8; 3]> {
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Canvas mode for Model space.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||||
|
pub enum ModelSpaceMode {
|
||||||
|
/// Canvas background dynamically follows the active UI theme.
|
||||||
|
#[default]
|
||||||
|
MatchTheme,
|
||||||
|
/// Classic CAD dark charcoal canvas (#212830) regardless of UI theme.
|
||||||
|
ClassicDark,
|
||||||
|
/// Custom background colors specified by the user.
|
||||||
|
Custom,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ModelSpaceMode {
|
||||||
|
pub const ALL: [Self; 3] = [Self::MatchTheme, Self::ClassicDark, Self::Custom];
|
||||||
|
|
||||||
|
pub fn label(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::MatchTheme => "Match Theme",
|
||||||
|
Self::ClassicDark => "Classic CAD Dark",
|
||||||
|
Self::Custom => "Custom",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Standard classic CAD dark charcoal background [33, 40, 48].
|
||||||
|
pub const CLASSIC_CAD_DARK_BG: [u8; 3] = [33, 40, 48];
|
||||||
|
|
||||||
|
/// Standard paper space sheet background [255, 255, 255].
|
||||||
|
pub const DEFAULT_PAPER_BG: [u8; 3] = [255, 255, 255];
|
||||||
|
|
||||||
|
/// Standard paper space desk surround [138, 138, 138].
|
||||||
|
pub const DEFAULT_DESK_BG: [u8; 3] = [138, 138, 138];
|
||||||
|
|
||||||
|
/// Persistent configuration for Model Space appearance and selection visual effects.
|
||||||
|
#[derive(Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct ModelSpaceThemeConfig {
|
||||||
|
/// How the canvas background is determined.
|
||||||
|
pub mode: ModelSpaceMode,
|
||||||
|
/// Custom model space canvas background [R, G, B] (0–255).
|
||||||
|
pub custom_bg: Option<[u8; 3]>,
|
||||||
|
/// Custom paper space sheet background [R, G, B] (0–255).
|
||||||
|
pub custom_paper_bg: Option<[u8; 3]>,
|
||||||
|
/// Custom paper space desk/surround background [R, G, B] (0–255).
|
||||||
|
pub custom_desk_bg: Option<[u8; 3]>,
|
||||||
|
/// Grid opacity percentage (5–100, default 18%).
|
||||||
|
pub grid_opacity: u8,
|
||||||
|
/// SELECTIONAREA: whether selection marquees have a translucent shaded fill.
|
||||||
|
pub selection_area: bool,
|
||||||
|
/// SELECTIONAREAOPACITY: transparency percentage of selection areas (0–100, default 12%).
|
||||||
|
pub selection_opacity: u8,
|
||||||
|
/// WINDOWSAREACOLOR: ACI index (0 = Theme Primary, 1..=255 = ACI).
|
||||||
|
pub selection_window_color: u8,
|
||||||
|
/// CROSSINGAREACOLOR: ACI index (0 = Theme Success, 1..=255 = ACI).
|
||||||
|
pub selection_crossing_color: u8,
|
||||||
|
/// SELECTIONEFFECTCOLOR: ACI index for selected entities highlight (0 = Theme Primary, 1..=255 = ACI).
|
||||||
|
pub selection_highlight_color: u8,
|
||||||
|
/// SELECTIONEFFECT: whether selected objects glow with solid highlight (true) or dash (false).
|
||||||
|
pub selection_effect: bool,
|
||||||
|
/// SELECTIONPREVIEW: rollover/hover highlight mode (0 = off, 1 = in cmd, 2 = idle, 3 = both).
|
||||||
|
pub selection_preview: u8,
|
||||||
|
/// GRIPSIZE: grip marker half-size in pixels (1–25, default 5).
|
||||||
|
pub grip_size: u8,
|
||||||
|
/// GRIPCOLOR: unselected grip ACI color (0 = Theme Primary outline, 1..=255 = ACI).
|
||||||
|
pub grip_color: u8,
|
||||||
|
/// GRIPHOT: selected/hot grip ACI color (0 = Theme Danger, 1..=255 = ACI).
|
||||||
|
pub grip_hot: u8,
|
||||||
|
/// GRIPHOVER: hovered/warm grip ACI color (0 = Theme Primary Strong, 1..=255 = ACI).
|
||||||
|
pub grip_hover: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ModelSpaceThemeConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
mode: ModelSpaceMode::MatchTheme,
|
||||||
|
custom_bg: None,
|
||||||
|
custom_paper_bg: None,
|
||||||
|
custom_desk_bg: None,
|
||||||
|
grid_opacity: 18,
|
||||||
|
selection_area: true,
|
||||||
|
selection_opacity: 12,
|
||||||
|
selection_window_color: 0,
|
||||||
|
selection_crossing_color: 0,
|
||||||
|
selection_highlight_color: 0,
|
||||||
|
selection_effect: true,
|
||||||
|
selection_preview: 3,
|
||||||
|
grip_size: 5,
|
||||||
|
grip_color: 0,
|
||||||
|
grip_hot: 0,
|
||||||
|
grip_hover: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ModelSpaceThemeConfig {
|
||||||
|
/// Resolve active model space canvas background as [f32; 4] based on current theme.
|
||||||
|
pub fn resolve_model_bg(&self, theme: &iced::Theme) -> [f32; 4] {
|
||||||
|
let rgb = match self.mode {
|
||||||
|
ModelSpaceMode::MatchTheme => theme_canvas_background(theme),
|
||||||
|
ModelSpaceMode::ClassicDark => CLASSIC_CAD_DARK_BG,
|
||||||
|
ModelSpaceMode::Custom => self.custom_bg.unwrap_or(CLASSIC_CAD_DARK_BG),
|
||||||
|
};
|
||||||
|
[
|
||||||
|
rgb[0] as f32 / 255.0,
|
||||||
|
rgb[1] as f32 / 255.0,
|
||||||
|
rgb[2] as f32 / 255.0,
|
||||||
|
1.0,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve active paper space sheet background as [f32; 4].
|
||||||
|
pub fn resolve_paper_bg(&self) -> [f32; 4] {
|
||||||
|
let rgb = self.custom_paper_bg.unwrap_or(DEFAULT_PAPER_BG);
|
||||||
|
[
|
||||||
|
rgb[0] as f32 / 255.0,
|
||||||
|
rgb[1] as f32 / 255.0,
|
||||||
|
rgb[2] as f32 / 255.0,
|
||||||
|
1.0,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve active paper space desk surround background as [f32; 4].
|
||||||
|
pub fn resolve_desk_bg(&self) -> [f32; 4] {
|
||||||
|
let rgb = self.custom_desk_bg.unwrap_or(DEFAULT_DESK_BG);
|
||||||
|
[
|
||||||
|
rgb[0] as f32 / 255.0,
|
||||||
|
rgb[1] as f32 / 255.0,
|
||||||
|
rgb[2] as f32 / 255.0,
|
||||||
|
1.0,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve active selection highlight overlay color as [f32; 4].
|
||||||
|
pub fn resolve_selection_color(&self) -> [f32; 4] {
|
||||||
|
if self.selection_highlight_color == 0 {
|
||||||
|
crate::scene::model::wire_model::WireModel::SELECTED
|
||||||
|
} else if let Some((r, g, b)) = acadrust::types::aci_table::aci_to_rgb(self.selection_highlight_color) {
|
||||||
|
[r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0]
|
||||||
|
} else {
|
||||||
|
crate::scene::model::wire_model::WireModel::SELECTED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Default canvas background for a given theme variant.
|
||||||
|
pub fn theme_canvas_background(theme: &iced::Theme) -> [u8; 3] {
|
||||||
|
match theme {
|
||||||
|
iced::Theme::Light => [255, 255, 255],
|
||||||
|
iced::Theme::SolarizedLight => [253, 246, 227],
|
||||||
|
iced::Theme::GruvboxLight => [251, 241, 199],
|
||||||
|
iced::Theme::TokyoNightLight => [225, 226, 231],
|
||||||
|
iced::Theme::KanagawaLotus => [242, 236, 222],
|
||||||
|
iced::Theme::Dark => [32, 34, 37],
|
||||||
|
iced::Theme::Dracula => [40, 42, 54],
|
||||||
|
iced::Theme::Nord => [46, 52, 64],
|
||||||
|
iced::Theme::SolarizedDark => [0, 43, 54],
|
||||||
|
iced::Theme::GruvboxDark => [40, 40, 40],
|
||||||
|
iced::Theme::TokyoNight => [26, 27, 38],
|
||||||
|
iced::Theme::TokyoNightStorm => [36, 40, 59],
|
||||||
|
iced::Theme::KanagawaWave => [31, 31, 40],
|
||||||
|
iced::Theme::KanagawaDragon => [24, 26, 28],
|
||||||
|
iced::Theme::Moonfly => [8, 18, 24],
|
||||||
|
iced::Theme::Nightfly => [1, 22, 39],
|
||||||
|
iced::Theme::Oxocarbon => [22, 22, 22],
|
||||||
|
iced::Theme::Ferra => [43, 41, 46],
|
||||||
|
_ => color_to_rgb(theme.palette().background.base.color),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a flexible theme name string into an `iced::Theme` variant.
|
||||||
|
/// Normalizes by stripping spaces, underscores, and dashes, case-insensitive.
|
||||||
|
pub fn parse_theme_name(s: &str) -> Option<iced::Theme> {
|
||||||
|
let clean: String = s
|
||||||
|
.chars()
|
||||||
|
.filter(|c| !c.is_whitespace() && *c != '_' && *c != '-')
|
||||||
|
.flat_map(|c| c.to_uppercase())
|
||||||
|
.collect();
|
||||||
|
match clean.as_str() {
|
||||||
|
"DARK" => Some(iced::Theme::Dark),
|
||||||
|
"LIGHT" => Some(iced::Theme::Light),
|
||||||
|
"DRACULA" => Some(iced::Theme::Dracula),
|
||||||
|
"NORD" => Some(iced::Theme::Nord),
|
||||||
|
"SOLARIZEDLIGHT" => Some(iced::Theme::SolarizedLight),
|
||||||
|
"SOLARIZEDDARK" => Some(iced::Theme::SolarizedDark),
|
||||||
|
"GRUVBOXLIGHT" => Some(iced::Theme::GruvboxLight),
|
||||||
|
"GRUVBOXDARK" => Some(iced::Theme::GruvboxDark),
|
||||||
|
"TOKYONIGHT" => Some(iced::Theme::TokyoNight),
|
||||||
|
"TOKYONIGHTSTORM" => Some(iced::Theme::TokyoNightStorm),
|
||||||
|
"TOKYONIGHTLIGHT" => Some(iced::Theme::TokyoNightLight),
|
||||||
|
"KANAGAWAWAVE" => Some(iced::Theme::KanagawaWave),
|
||||||
|
"KANAGAWADRAGON" => Some(iced::Theme::KanagawaDragon),
|
||||||
|
"KANAGAWALOTUS" => Some(iced::Theme::KanagawaLotus),
|
||||||
|
"MOONFLY" => Some(iced::Theme::Moonfly),
|
||||||
|
"NIGHTFLY" => Some(iced::Theme::Nightfly),
|
||||||
|
"OXOCARBON" => Some(iced::Theme::Oxocarbon),
|
||||||
|
"FERRA" => Some(iced::Theme::Ferra),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Clone, PartialEq, Serialize, Deserialize)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub struct RecentConfig {
|
pub struct RecentConfig {
|
||||||
@@ -286,3 +489,100 @@ const WEB_CONFIG_KEY: &str = "opencadstudio.settings";
|
|||||||
fn config_path() -> Option<std::path::PathBuf> {
|
fn config_path() -> Option<std::path::PathBuf> {
|
||||||
Some(crate::config::config_dir()?.join("settings.json"))
|
Some(crate::config::config_dir()?.join("settings.json"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_model_space_defaults() {
|
||||||
|
let cfg = ModelSpaceThemeConfig::default();
|
||||||
|
assert_eq!(cfg.mode, ModelSpaceMode::MatchTheme);
|
||||||
|
assert_eq!(cfg.custom_bg, None);
|
||||||
|
assert_eq!(cfg.custom_paper_bg, None);
|
||||||
|
assert_eq!(cfg.grid_opacity, 18);
|
||||||
|
assert!(cfg.selection_area);
|
||||||
|
assert_eq!(cfg.selection_opacity, 12);
|
||||||
|
assert_eq!(cfg.selection_window_color, 0);
|
||||||
|
assert_eq!(cfg.selection_crossing_color, 0);
|
||||||
|
assert_eq!(cfg.selection_highlight_color, 0);
|
||||||
|
assert!(cfg.selection_effect);
|
||||||
|
assert_eq!(cfg.selection_preview, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_resolve_model_bg_match_theme() {
|
||||||
|
let cfg = ModelSpaceThemeConfig {
|
||||||
|
mode: ModelSpaceMode::MatchTheme,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
// Light theme should produce pure white background [1.0, 1.0, 1.0, 1.0]
|
||||||
|
let light_bg = cfg.resolve_model_bg(&iced::Theme::Light);
|
||||||
|
assert_eq!(light_bg, [1.0, 1.0, 1.0, 1.0]);
|
||||||
|
|
||||||
|
// Oxocarbon theme should produce dark background
|
||||||
|
let oxo_bg = cfg.resolve_model_bg(&iced::Theme::Oxocarbon);
|
||||||
|
assert!((oxo_bg[0] - 22.0 / 255.0).abs() < 1e-4);
|
||||||
|
assert!((oxo_bg[1] - 22.0 / 255.0).abs() < 1e-4);
|
||||||
|
assert!((oxo_bg[2] - 22.0 / 255.0).abs() < 1e-4);
|
||||||
|
assert_eq!(oxo_bg[3], 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_resolve_model_bg_classic_dark() {
|
||||||
|
let cfg = ModelSpaceThemeConfig {
|
||||||
|
mode: ModelSpaceMode::ClassicDark,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
// Even with Light theme active, ClassicDark must stay locked to [33, 40, 48]
|
||||||
|
let bg = cfg.resolve_model_bg(&iced::Theme::Light);
|
||||||
|
assert!((bg[0] - 33.0 / 255.0).abs() < 1e-4);
|
||||||
|
assert!((bg[1] - 40.0 / 255.0).abs() < 1e-4);
|
||||||
|
assert!((bg[2] - 48.0 / 255.0).abs() < 1e-4);
|
||||||
|
assert_eq!(bg[3], 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_resolve_model_bg_custom() {
|
||||||
|
let cfg = ModelSpaceThemeConfig {
|
||||||
|
mode: ModelSpaceMode::Custom,
|
||||||
|
custom_bg: Some([10, 20, 30]),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let bg = cfg.resolve_model_bg(&iced::Theme::Light);
|
||||||
|
assert!((bg[0] - 10.0 / 255.0).abs() < 1e-4);
|
||||||
|
assert!((bg[1] - 20.0 / 255.0).abs() < 1e-4);
|
||||||
|
assert!((bg[2] - 30.0 / 255.0).abs() < 1e-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_model_space_config_serde_roundtrip() {
|
||||||
|
let mut original = AppConfig::default();
|
||||||
|
original.model_space.mode = ModelSpaceMode::Custom;
|
||||||
|
original.model_space.custom_bg = Some([12, 34, 56]);
|
||||||
|
original.model_space.grid_opacity = 45;
|
||||||
|
original.model_space.selection_opacity = 35;
|
||||||
|
original.model_space.selection_window_color = 5;
|
||||||
|
|
||||||
|
let serialized = serde_json::to_string(&original).expect("serialize config");
|
||||||
|
let deserialized: AppConfig = serde_json::from_str(&serialized).expect("deserialize config");
|
||||||
|
|
||||||
|
assert_eq!(deserialized.model_space.mode, ModelSpaceMode::Custom);
|
||||||
|
assert_eq!(deserialized.model_space.custom_bg, Some([12, 34, 56]));
|
||||||
|
assert_eq!(deserialized.model_space.grid_opacity, 45);
|
||||||
|
assert_eq!(deserialized.model_space.selection_opacity, 35);
|
||||||
|
assert_eq!(deserialized.model_space.selection_window_color, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_theme_name() {
|
||||||
|
assert_eq!(parse_theme_name("light"), Some(iced::Theme::Light));
|
||||||
|
assert_eq!(parse_theme_name("DARK"), Some(iced::Theme::Dark));
|
||||||
|
assert_eq!(parse_theme_name("solarized light"), Some(iced::Theme::SolarizedLight));
|
||||||
|
assert_eq!(parse_theme_name("tokyo-night-storm"), Some(iced::Theme::TokyoNightStorm));
|
||||||
|
assert_eq!(parse_theme_name("Kanagawa_Lotus"), Some(iced::Theme::KanagawaLotus));
|
||||||
|
assert_eq!(parse_theme_name("1"), None);
|
||||||
|
assert_eq!(parse_theme_name("0"), None);
|
||||||
|
assert_eq!(parse_theme_name("nonexistent_theme"), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -977,6 +977,11 @@ pub(super) struct OpenCADStudio {
|
|||||||
active_theme: Theme,
|
active_theme: Theme,
|
||||||
ui_theme: config::UiThemeConfig,
|
ui_theme: config::UiThemeConfig,
|
||||||
theme_color_inputs: [String; 6],
|
theme_color_inputs: [String; 6],
|
||||||
|
model_space: config::ModelSpaceThemeConfig,
|
||||||
|
model_bg_input: String,
|
||||||
|
paper_bg_input: String,
|
||||||
|
desk_bg_input: String,
|
||||||
|
pub(crate) saved_custom_palette: Option<config::UiThemePalette>,
|
||||||
|
|
||||||
// ── Keyboard Shortcut Editor ──────────────────────────────────────────
|
// ── Keyboard Shortcut Editor ──────────────────────────────────────────
|
||||||
/// Complete editable key → command/action table.
|
/// Complete editable key → command/action table.
|
||||||
@@ -1928,6 +1933,38 @@ pub enum Message {
|
|||||||
OptionsThemeChanged(String),
|
OptionsThemeChanged(String),
|
||||||
/// Edit one of Custom theme's six base colours as #RRGGBB.
|
/// Edit one of Custom theme's six base colours as #RRGGBB.
|
||||||
OptionsThemeColorChanged(usize, String),
|
OptionsThemeColorChanged(usize, String),
|
||||||
|
/// Change Model Space canvas mode (MatchTheme, ClassicDark, Custom).
|
||||||
|
ModelSpaceModeChanged(config::ModelSpaceMode),
|
||||||
|
/// Change Model Space custom background color as hex or empty for default.
|
||||||
|
ModelSpaceBgChanged(String),
|
||||||
|
/// Change Paper Space custom sheet background color as hex or empty for default.
|
||||||
|
PaperSpaceBgChanged(String),
|
||||||
|
/// Change Paper Space desk surround background (#RRGGBB).
|
||||||
|
DeskSpaceBgChanged(String),
|
||||||
|
/// Change Grid opacity percentage (5–100).
|
||||||
|
GridOpacityChanged(u8),
|
||||||
|
/// Change Selection Area indicator toggle (SELECTIONAREA).
|
||||||
|
SelectionAreaToggled(bool),
|
||||||
|
/// Change Selection Area opacity percentage (0–100, SELECTIONAREAOPACITY).
|
||||||
|
SelectionOpacityChanged(u8),
|
||||||
|
/// Change Window selection color ACI index (0 = Theme Primary, 1..=255 = ACI).
|
||||||
|
SelectionWindowColorChanged(u8),
|
||||||
|
/// Change Crossing selection color ACI index (0 = Theme Success, 1..=255 = ACI).
|
||||||
|
SelectionCrossingColorChanged(u8),
|
||||||
|
/// Change Selection Highlight color ACI index (0 = Theme Primary, 1..=255 = ACI).
|
||||||
|
SelectionHighlightColorChanged(u8),
|
||||||
|
/// Change Grip size in pixels (1–25, GRIPSIZE).
|
||||||
|
GripSizeChanged(u8),
|
||||||
|
/// Change Unselected Grip color ACI index (0 = Theme Primary, 1..=255 = ACI, GRIPCOLOR).
|
||||||
|
GripColorChanged(u8),
|
||||||
|
/// Change Selected/Hot Grip color ACI index (0 = Theme Danger, 1..=255 = ACI, GRIPHOT).
|
||||||
|
GripHotChanged(u8),
|
||||||
|
/// Change Hover/Warm Grip color ACI index (0 = Theme Primary Strong, 1..=255 = ACI, GRIPHOVER).
|
||||||
|
GripHoverChanged(u8),
|
||||||
|
/// Restore Model Space display/canvas appearance to defaults.
|
||||||
|
RestoreModelSpaceDisplayDefaults,
|
||||||
|
/// Restore Selection visual effect settings to defaults.
|
||||||
|
RestoreSelectionVisualDefaults,
|
||||||
/// Register or unregister as the .dwg/.dxf handler, from Options. Same
|
/// Register or unregister as the .dwg/.dxf handler, from Options. Same
|
||||||
/// setting the FILEASSOC command carries.
|
/// setting the FILEASSOC command carries.
|
||||||
FileAssocChanged(bool),
|
FileAssocChanged(bool),
|
||||||
@@ -3472,6 +3509,11 @@ impl OpenCADStudio {
|
|||||||
active_theme: Theme::Oxocarbon,
|
active_theme: Theme::Oxocarbon,
|
||||||
ui_theme: config::UiThemeConfig::default(),
|
ui_theme: config::UiThemeConfig::default(),
|
||||||
theme_color_inputs: config::UiThemePalette::default().hex_values(),
|
theme_color_inputs: config::UiThemePalette::default().hex_values(),
|
||||||
|
model_space: config::ModelSpaceThemeConfig::default(),
|
||||||
|
model_bg_input: String::new(),
|
||||||
|
paper_bg_input: String::new(),
|
||||||
|
desk_bg_input: String::new(),
|
||||||
|
saved_custom_palette: None,
|
||||||
// Keyboard shortcuts
|
// Keyboard shortcuts
|
||||||
shortcut_bindings: rustc_hash::FxHashMap::default(),
|
shortcut_bindings: rustc_hash::FxHashMap::default(),
|
||||||
shortcut_editor_rows: Vec::new(),
|
shortcut_editor_rows: Vec::new(),
|
||||||
|
|||||||
+55
-4
@@ -393,8 +393,8 @@ impl OpenCADStudio {
|
|||||||
pick_add: self.pick_add,
|
pick_add: self.pick_add,
|
||||||
pick_drag_rect: self.pick_drag_rect,
|
pick_drag_rect: self.pick_drag_rect,
|
||||||
quick_properties: self.quick_properties,
|
quick_properties: self.quick_properties,
|
||||||
bg_color: self.default_bg_color.map(f4_to_u3),
|
bg_color: None,
|
||||||
paper_bg_color: self.default_paper_bg_color.map(f4_to_u3),
|
paper_bg_color: None,
|
||||||
language: self.language,
|
language: self.language,
|
||||||
cliprompt_lines: crate::app::settings::clamp_clipromptlines(self.cliprompt_lines),
|
cliprompt_lines: crate::app::settings::clamp_clipromptlines(self.cliprompt_lines),
|
||||||
block_mru: self.block_mru.clone(),
|
block_mru: self.block_mru.clone(),
|
||||||
@@ -455,8 +455,7 @@ impl OpenCADStudio {
|
|||||||
self.pick_add = s.pick_add;
|
self.pick_add = s.pick_add;
|
||||||
self.pick_drag_rect = s.pick_drag_rect;
|
self.pick_drag_rect = s.pick_drag_rect;
|
||||||
self.quick_properties = s.quick_properties;
|
self.quick_properties = s.quick_properties;
|
||||||
self.default_bg_color = s.bg_color.map(u3_to_f4);
|
// Legacy settings.bg_color / paper_bg_color are superseded by model_space config.
|
||||||
self.default_paper_bg_color = s.paper_bg_color.map(u3_to_f4);
|
|
||||||
if crate::i18n::set_language(s.language).is_ok() {
|
if crate::i18n::set_language(s.language).is_ok() {
|
||||||
self.language = s.language;
|
self.language = s.language;
|
||||||
}
|
}
|
||||||
@@ -775,6 +774,7 @@ impl OpenCADStudio {
|
|||||||
.map(|(key, command)| (key.clone(), command.clone()))
|
.map(|(key, command)| (key.clone(), command.clone()))
|
||||||
.collect(),
|
.collect(),
|
||||||
},
|
},
|
||||||
|
model_space: self.model_space.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -784,6 +784,23 @@ impl OpenCADStudio {
|
|||||||
self.ui_theme = cfg.theme.clone();
|
self.ui_theme = cfg.theme.clone();
|
||||||
self.active_theme = self.ui_theme.to_iced();
|
self.active_theme = self.ui_theme.to_iced();
|
||||||
self.theme_color_inputs = self.ui_theme.palette.hex_values();
|
self.theme_color_inputs = self.ui_theme.palette.hex_values();
|
||||||
|
self.model_space = cfg.model_space;
|
||||||
|
self.model_bg_input = self
|
||||||
|
.model_space
|
||||||
|
.custom_bg
|
||||||
|
.map(crate::app::config::rgb_to_hex)
|
||||||
|
.unwrap_or_default();
|
||||||
|
self.paper_bg_input = self
|
||||||
|
.model_space
|
||||||
|
.custom_paper_bg
|
||||||
|
.map(crate::app::config::rgb_to_hex)
|
||||||
|
.unwrap_or_default();
|
||||||
|
self.desk_bg_input = self
|
||||||
|
.model_space
|
||||||
|
.custom_desk_bg
|
||||||
|
.map(crate::app::config::rgb_to_hex)
|
||||||
|
.unwrap_or_default();
|
||||||
|
self.sync_model_space_theme(false);
|
||||||
self.recent_files = cfg
|
self.recent_files = cfg
|
||||||
.recent
|
.recent
|
||||||
.files
|
.files
|
||||||
@@ -831,6 +848,40 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sync the active theme and model space configuration into all open tabs
|
||||||
|
/// and the application default background settings.
|
||||||
|
pub(in crate::app) fn sync_model_space_theme(&mut self, geometry_changed: bool) {
|
||||||
|
let model_bg = self.model_space.resolve_model_bg(&self.active_theme);
|
||||||
|
let paper_bg = self.model_space.resolve_paper_bg();
|
||||||
|
let sel_color = self.model_space.resolve_selection_color();
|
||||||
|
let sel_effect = self.model_space.selection_effect;
|
||||||
|
self.default_bg_color = Some(model_bg);
|
||||||
|
self.default_paper_bg_color = Some(paper_bg);
|
||||||
|
|
||||||
|
for tab in &mut self.tabs {
|
||||||
|
let old_bg = tab.scene.bg_color;
|
||||||
|
let old_paper = tab.scene.paper_bg_color;
|
||||||
|
let old_sel_color = tab.scene.selection_color;
|
||||||
|
let old_sel_effect = tab.scene.selection_effect;
|
||||||
|
tab.scene.bg_color = model_bg;
|
||||||
|
tab.scene.paper_bg_color = paper_bg;
|
||||||
|
tab.scene.selection_color = sel_color;
|
||||||
|
tab.scene.selection_effect = sel_effect;
|
||||||
|
tab.bg_color = Some(model_bg);
|
||||||
|
tab.paper_bg_color = Some(paper_bg);
|
||||||
|
|
||||||
|
if old_sel_color != sel_color || old_sel_effect != sel_effect {
|
||||||
|
tab.scene.selection_generation = tab.scene.selection_generation.wrapping_add(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if geometry_changed || old_bg != model_bg || old_paper != paper_bg {
|
||||||
|
tab.scene.recolor_meshes();
|
||||||
|
tab.scene.bump_geometry();
|
||||||
|
}
|
||||||
|
tab.scene.request_refresh(crate::scene::ViewportRefreshScope::All);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Record that the one-time default-association prompt has been answered and
|
/// Record that the one-time default-association prompt has been answered and
|
||||||
/// flush it to disk, so the dialog never reappears on later launches.
|
/// flush it to disk, so the dialog never reappears on later launches.
|
||||||
pub(in crate::app) fn mark_assoc_prompted(&mut self) {
|
pub(in crate::app) fn mark_assoc_prompted(&mut self) {
|
||||||
|
|||||||
+171
-12
@@ -726,12 +726,9 @@ impl OpenCADStudio {
|
|||||||
let size_label = format_size(size_bytes);
|
let size_label = format_size(size_bytes);
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_info(crate::tf!("Opening \"{name}\" ({size_label})…").as_ref());
|
.push_info(crate::tf!("Opening \"{name}\" ({size_label})…").as_ref());
|
||||||
let model_bg = self.default_bg_color.unwrap_or([
|
let model_bg = self.default_bg_color.unwrap_or_else(|| {
|
||||||
33.0 / 255.0,
|
self.model_space.resolve_model_bg(&self.active_theme)
|
||||||
40.0 / 255.0,
|
});
|
||||||
48.0 / 255.0,
|
|
||||||
1.0,
|
|
||||||
]);
|
|
||||||
Task::perform(
|
Task::perform(
|
||||||
crate::io::open_path_with_phase(path, progress, model_bg),
|
crate::io::open_path_with_phase(path, progress, model_bg),
|
||||||
move |result| Message::FileOpened(open_id, result),
|
move |result| Message::FileOpened(open_id, result),
|
||||||
@@ -5564,11 +5561,15 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Message::SetTheme(theme) => {
|
Message::SetTheme(theme) => {
|
||||||
|
if self.ui_theme.name == "Custom" {
|
||||||
|
self.saved_custom_palette = Some(self.ui_theme.palette);
|
||||||
|
}
|
||||||
self.ui_theme.name = theme.to_string();
|
self.ui_theme.name = theme.to_string();
|
||||||
self.ui_theme.palette =
|
self.ui_theme.palette =
|
||||||
crate::app::config::UiThemePalette::from_iced(theme.seed());
|
crate::app::config::UiThemePalette::from_iced(theme.seed());
|
||||||
self.theme_color_inputs = self.ui_theme.palette.hex_values();
|
self.theme_color_inputs = self.ui_theme.palette.hex_values();
|
||||||
self.active_theme = theme;
|
self.active_theme = theme;
|
||||||
|
self.sync_model_space_theme(true);
|
||||||
self.persist_settings_if_changed();
|
self.persist_settings_if_changed();
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
@@ -5817,6 +5818,9 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Message::OptionsThemeChanged(name) => {
|
Message::OptionsThemeChanged(name) => {
|
||||||
|
if self.ui_theme.name == "Custom" && name != "Custom" {
|
||||||
|
self.saved_custom_palette = Some(self.ui_theme.palette);
|
||||||
|
}
|
||||||
self.ui_theme.name = name;
|
self.ui_theme.name = name;
|
||||||
if let Some(theme) =
|
if let Some(theme) =
|
||||||
crate::app::config::builtin_theme(&self.ui_theme.name)
|
crate::app::config::builtin_theme(&self.ui_theme.name)
|
||||||
@@ -5827,8 +5831,13 @@ impl OpenCADStudio {
|
|||||||
self.active_theme = theme;
|
self.active_theme = theme;
|
||||||
} else {
|
} else {
|
||||||
self.ui_theme.name = "Custom".to_string();
|
self.ui_theme.name = "Custom".to_string();
|
||||||
|
if let Some(saved) = self.saved_custom_palette {
|
||||||
|
self.ui_theme.palette = saved;
|
||||||
|
self.theme_color_inputs = saved.hex_values();
|
||||||
|
}
|
||||||
self.active_theme = self.ui_theme.to_iced();
|
self.active_theme = self.ui_theme.to_iced();
|
||||||
}
|
}
|
||||||
|
self.sync_model_space_theme(true);
|
||||||
self.persist_settings_if_changed();
|
self.persist_settings_if_changed();
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
@@ -5841,11 +5850,164 @@ impl OpenCADStudio {
|
|||||||
if self.ui_theme.palette.set_hex(index, &value) {
|
if self.ui_theme.palette.set_hex(index, &value) {
|
||||||
self.ui_theme.name = "Custom".to_string();
|
self.ui_theme.name = "Custom".to_string();
|
||||||
self.active_theme = self.ui_theme.to_iced();
|
self.active_theme = self.ui_theme.to_iced();
|
||||||
|
self.sync_model_space_theme(true);
|
||||||
self.persist_settings_if_changed();
|
self.persist_settings_if_changed();
|
||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Message::ModelSpaceModeChanged(mode) => {
|
||||||
|
self.model_space.mode = mode;
|
||||||
|
self.sync_model_space_theme(true);
|
||||||
|
self.persist_settings_if_changed();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::ModelSpaceBgChanged(hex) => {
|
||||||
|
self.model_bg_input = hex.clone();
|
||||||
|
if hex.trim().is_empty() {
|
||||||
|
self.model_space.custom_bg = None;
|
||||||
|
self.model_space.mode = crate::app::config::ModelSpaceMode::MatchTheme;
|
||||||
|
self.sync_model_space_theme(true);
|
||||||
|
self.persist_settings_if_changed();
|
||||||
|
} else if let Some(rgb) = crate::app::config::parse_hex(&hex) {
|
||||||
|
self.model_space.custom_bg = Some(rgb);
|
||||||
|
self.model_space.mode = crate::app::config::ModelSpaceMode::Custom;
|
||||||
|
self.sync_model_space_theme(true);
|
||||||
|
self.persist_settings_if_changed();
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::PaperSpaceBgChanged(hex) => {
|
||||||
|
self.paper_bg_input = hex.clone();
|
||||||
|
if hex.trim().is_empty() {
|
||||||
|
self.model_space.custom_paper_bg = None;
|
||||||
|
self.sync_model_space_theme(true);
|
||||||
|
self.persist_settings_if_changed();
|
||||||
|
} else if let Some(rgb) = crate::app::config::parse_hex(&hex) {
|
||||||
|
self.model_space.custom_paper_bg = Some(rgb);
|
||||||
|
self.sync_model_space_theme(true);
|
||||||
|
self.persist_settings_if_changed();
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::DeskSpaceBgChanged(hex) => {
|
||||||
|
self.desk_bg_input = hex.clone();
|
||||||
|
if hex.trim().is_empty() {
|
||||||
|
self.model_space.custom_desk_bg = None;
|
||||||
|
self.sync_model_space_theme(false);
|
||||||
|
self.persist_settings_if_changed();
|
||||||
|
} else if let Some(rgb) = crate::app::config::parse_hex(&hex) {
|
||||||
|
self.model_space.custom_desk_bg = Some(rgb);
|
||||||
|
self.sync_model_space_theme(false);
|
||||||
|
self.persist_settings_if_changed();
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::GridOpacityChanged(opacity) => {
|
||||||
|
self.model_space.grid_opacity = opacity.clamp(5, 100);
|
||||||
|
self.sync_model_space_theme(false);
|
||||||
|
self.persist_settings_if_changed();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::SelectionAreaToggled(enabled) => {
|
||||||
|
self.model_space.selection_area = enabled;
|
||||||
|
self.sync_model_space_theme(false);
|
||||||
|
self.persist_settings_if_changed();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::SelectionOpacityChanged(opacity) => {
|
||||||
|
self.model_space.selection_opacity = opacity.clamp(0, 100);
|
||||||
|
self.sync_model_space_theme(false);
|
||||||
|
self.persist_settings_if_changed();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::SelectionWindowColorChanged(color) => {
|
||||||
|
self.model_space.selection_window_color = color;
|
||||||
|
self.sync_model_space_theme(false);
|
||||||
|
self.persist_settings_if_changed();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::SelectionCrossingColorChanged(color) => {
|
||||||
|
self.model_space.selection_crossing_color = color;
|
||||||
|
self.sync_model_space_theme(false);
|
||||||
|
self.persist_settings_if_changed();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::SelectionHighlightColorChanged(color) => {
|
||||||
|
self.model_space.selection_highlight_color = color;
|
||||||
|
self.sync_model_space_theme(false);
|
||||||
|
self.persist_settings_if_changed();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::GripSizeChanged(size) => {
|
||||||
|
self.model_space.grip_size = size.clamp(1, 25);
|
||||||
|
self.sync_model_space_theme(false);
|
||||||
|
self.persist_settings_if_changed();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::GripColorChanged(color) => {
|
||||||
|
self.model_space.grip_color = color;
|
||||||
|
self.sync_model_space_theme(false);
|
||||||
|
self.persist_settings_if_changed();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::GripHotChanged(color) => {
|
||||||
|
self.model_space.grip_hot = color;
|
||||||
|
self.sync_model_space_theme(false);
|
||||||
|
self.persist_settings_if_changed();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::GripHoverChanged(color) => {
|
||||||
|
self.model_space.grip_hover = color;
|
||||||
|
self.sync_model_space_theme(false);
|
||||||
|
self.persist_settings_if_changed();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::RestoreModelSpaceDisplayDefaults => {
|
||||||
|
self.model_space.mode = crate::app::config::ModelSpaceMode::MatchTheme;
|
||||||
|
self.model_space.custom_bg = None;
|
||||||
|
self.model_space.custom_paper_bg = None;
|
||||||
|
self.model_space.custom_desk_bg = None;
|
||||||
|
self.model_space.grid_opacity = 18;
|
||||||
|
self.model_bg_input.clear();
|
||||||
|
self.paper_bg_input.clear();
|
||||||
|
self.desk_bg_input.clear();
|
||||||
|
self.sync_model_space_theme(true);
|
||||||
|
self.persist_settings_if_changed();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::RestoreSelectionVisualDefaults => {
|
||||||
|
self.model_space.selection_area = true;
|
||||||
|
self.model_space.selection_opacity = 12;
|
||||||
|
self.model_space.selection_window_color = 0;
|
||||||
|
self.model_space.selection_crossing_color = 0;
|
||||||
|
self.model_space.selection_highlight_color = 0;
|
||||||
|
self.model_space.selection_effect = true;
|
||||||
|
self.model_space.selection_preview = 3;
|
||||||
|
self.model_space.grip_size = 5;
|
||||||
|
self.model_space.grip_color = 0;
|
||||||
|
self.model_space.grip_hot = 0;
|
||||||
|
self.model_space.grip_hover = 0;
|
||||||
|
self.sync_model_space_theme(false);
|
||||||
|
self.persist_settings_if_changed();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
Message::FileAssocChanged(enabled) => {
|
Message::FileAssocChanged(enabled) => {
|
||||||
// The same two calls FILEASSOC makes, so the checkbox and the
|
// The same two calls FILEASSOC makes, so the checkbox and the
|
||||||
// command cannot leave the setting and the registration
|
// command cannot leave the setting and the registration
|
||||||
@@ -5943,12 +6105,9 @@ impl OpenCADStudio {
|
|||||||
self.close_active_modal();
|
self.close_active_modal();
|
||||||
return Task::none();
|
return Task::none();
|
||||||
};
|
};
|
||||||
let model_bg = self.default_bg_color.unwrap_or([
|
let model_bg = self.default_bg_color.unwrap_or_else(|| {
|
||||||
33.0 / 255.0,
|
self.model_space.resolve_model_bg(&self.active_theme)
|
||||||
40.0 / 255.0,
|
});
|
||||||
48.0 / 255.0,
|
|
||||||
1.0,
|
|
||||||
]);
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
if let Some(path) = opening.source_path.clone() {
|
if let Some(path) = opening.source_path.clone() {
|
||||||
let current_fingerprint =
|
let current_fingerprint =
|
||||||
|
|||||||
@@ -96,19 +96,3 @@ pub(super) fn parse_plot_scale(s: &str) -> (f64, f64) {
|
|||||||
}
|
}
|
||||||
(1.0, 1.0)
|
(1.0, 1.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert an internal `[r,g,b,a]` colour (0.0–1.0) to a persisted 0–255 RGB
|
|
||||||
/// triplet, dropping alpha (backgrounds are always opaque).
|
|
||||||
pub(super) fn f4_to_u3([r, g, b, _]: [f32; 4]) -> [u8; 3] {
|
|
||||||
[
|
|
||||||
(r * 255.0).round().clamp(0.0, 255.0) as u8,
|
|
||||||
(g * 255.0).round().clamp(0.0, 255.0) as u8,
|
|
||||||
(b * 255.0).round().clamp(0.0, 255.0) as u8,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Inverse of [`f4_to_u3`]: a persisted 0–255 RGB triplet back to an opaque
|
|
||||||
/// `[r,g,b,a]` colour.
|
|
||||||
pub(super) fn u3_to_f4([r, g, b]: [u8; 3]) -> [f32; 4] {
|
|
||||||
[r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0]
|
|
||||||
}
|
|
||||||
@@ -2270,13 +2270,17 @@ impl OpenCADStudio {
|
|||||||
)
|
)
|
||||||
})?
|
})?
|
||||||
});
|
});
|
||||||
let highlights_hover = self.tabs[i]
|
let preview_in_command = (self.model_space.selection_preview & 2) != 0;
|
||||||
.active_cmd
|
let highlights_hover = preview_in_command
|
||||||
.as_ref()
|
&& self.tabs[i]
|
||||||
.map(|c| c.entity_pick_highlights_hover())
|
.active_cmd
|
||||||
.unwrap_or(false);
|
.as_ref()
|
||||||
|
.map(|c| c.entity_pick_highlights_hover())
|
||||||
|
.unwrap_or(false);
|
||||||
if highlights_hover {
|
if highlights_hover {
|
||||||
self.tabs[i].scene.set_hover_highlight(hovered);
|
self.tabs[i].scene.set_hover_highlight(hovered);
|
||||||
|
} else {
|
||||||
|
self.tabs[i].scene.set_hover_highlight(None);
|
||||||
}
|
}
|
||||||
let hover_handle = hovered.unwrap_or(acadrust::Handle::NULL);
|
let hover_handle = hovered.unwrap_or(acadrust::Handle::NULL);
|
||||||
let shift = self.shift_down;
|
let shift = self.shift_down;
|
||||||
@@ -5118,7 +5122,12 @@ impl OpenCADStudio {
|
|||||||
);
|
);
|
||||||
solid_ms = started.elapsed().as_secs_f64() * 1000.0;
|
solid_ms = started.elapsed().as_secs_f64() * 1000.0;
|
||||||
}
|
}
|
||||||
self.tabs[i].scene.set_hover_highlight(hovered);
|
let preview_idle = (self.model_space.selection_preview & 1) != 0;
|
||||||
|
if preview_idle {
|
||||||
|
self.tabs[i].scene.set_hover_highlight(hovered);
|
||||||
|
} else {
|
||||||
|
self.tabs[i].scene.set_hover_highlight(None);
|
||||||
|
}
|
||||||
self.hover_dwell = None;
|
self.hover_dwell = None;
|
||||||
let hover_ms = hover_started.elapsed().as_secs_f64() * 1000.0;
|
let hover_ms = hover_started.elapsed().as_secs_f64() * 1000.0;
|
||||||
if perf && hover_ms >= 5.0 {
|
if perf && hover_ms >= 5.0 {
|
||||||
|
|||||||
+38
-30
@@ -35,28 +35,13 @@ pub(in crate::app) use overlay::{MTEXT_TEXT_ID, TEXT_INLINE_ID};
|
|||||||
pub(in crate::app) const VIEWPORT_CAPTURE_BOUNDS_ID: &str = "viewport-capture-bounds";
|
pub(in crate::app) const VIEWPORT_CAPTURE_BOUNDS_ID: &str = "viewport-capture-bounds";
|
||||||
|
|
||||||
const VIEWCUBE_HIT_SIZE: f32 = VIEWCUBE_REGION_PX;
|
const VIEWCUBE_HIT_SIZE: f32 = VIEWCUBE_REGION_PX;
|
||||||
/// The desk shown around the sheet, as the widget wants it. One definition,
|
|
||||||
/// shared with the renderer that clears the sheet viewport to the same thing.
|
|
||||||
const PAPER_SPACE_BACKGROUND: Color = Color {
|
|
||||||
r: crate::scene::PAPER_DESK_COLOR[0],
|
|
||||||
g: crate::scene::PAPER_DESK_COLOR[1],
|
|
||||||
b: crate::scene::PAPER_DESK_COLOR[2],
|
|
||||||
a: crate::scene::PAPER_DESK_COLOR[3],
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Base surface directly under the crosshair. Paper content viewports render
|
/// Base surface directly under the crosshair. Paper content viewports render
|
||||||
/// transparently over the sheet/desk, including while MSPACE input is active.
|
/// transparently over the sheet/desk, including while MSPACE input is active.
|
||||||
fn crosshair_background(tab: &DocumentTab, is_paper: bool) -> [f32; 4] {
|
fn crosshair_background(tab: &DocumentTab, is_paper: bool, desk: [f32; 4]) -> [f32; 4] {
|
||||||
if !is_paper {
|
if !is_paper {
|
||||||
return tab.scene.bg_color;
|
return tab.scene.bg_color;
|
||||||
}
|
}
|
||||||
|
|
||||||
let desk = [
|
|
||||||
PAPER_SPACE_BACKGROUND.r,
|
|
||||||
PAPER_SPACE_BACKGROUND.g,
|
|
||||||
PAPER_SPACE_BACKGROUND.b,
|
|
||||||
PAPER_SPACE_BACKGROUND.a,
|
|
||||||
];
|
|
||||||
let (cursor, viewport_size) = {
|
let (cursor, viewport_size) = {
|
||||||
let selection = tab.scene.selection.borrow();
|
let selection = tab.scene.selection.borrow();
|
||||||
(selection.last_move_pos, selection.vp_size)
|
(selection.last_move_pos, selection.vp_size)
|
||||||
@@ -428,7 +413,14 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
crate::ui::overlay::grid_overlay(grid)
|
let desk_bg = self.model_space.resolve_desk_bg();
|
||||||
|
let bg = crosshair_background(tab, is_paper, desk_bg);
|
||||||
|
let bg_lum = 0.299 * bg[0] + 0.587 * bg[1] + 0.114 * bg[2];
|
||||||
|
let grid_style = crate::ui::overlay::GridStyle {
|
||||||
|
opacity: self.model_space.grid_opacity,
|
||||||
|
bg_luminance: bg_lum,
|
||||||
|
};
|
||||||
|
crate::ui::overlay::grid_overlay(grid, grid_style)
|
||||||
};
|
};
|
||||||
|
|
||||||
let selection_overlay = {
|
let selection_overlay = {
|
||||||
@@ -777,7 +769,7 @@ impl OpenCADStudio {
|
|||||||
tab.pan_mode || tab.orbit_mode || tab.zoom_dynamic_mode,
|
tab.pan_mode || tab.orbit_mode || tab.zoom_dynamic_mode,
|
||||||
self.ribbon.open_dropdown.is_some(),
|
self.ribbon.open_dropdown.is_some(),
|
||||||
hover_locked,
|
hover_locked,
|
||||||
crosshair_background(tab, is_paper),
|
crosshair_background(tab, is_paper, self.model_space.resolve_desk_bg()),
|
||||||
crate::ui::overlay::CrosshairOptions {
|
crate::ui::overlay::CrosshairOptions {
|
||||||
size_percent: self.cursor_size,
|
size_percent: self.cursor_size,
|
||||||
pick_box: self.pick_box,
|
pick_box: self.pick_box,
|
||||||
@@ -787,6 +779,17 @@ impl OpenCADStudio {
|
|||||||
iso_plane: self.iso_plane,
|
iso_plane: self.iso_plane,
|
||||||
snap_angle_deg: self.snap_angle_deg,
|
snap_angle_deg: self.snap_angle_deg,
|
||||||
},
|
},
|
||||||
|
crate::ui::overlay::SelectionVisualOptions {
|
||||||
|
area: self.model_space.selection_area,
|
||||||
|
opacity: self.model_space.selection_opacity,
|
||||||
|
window_color: self.model_space.selection_window_color,
|
||||||
|
crossing_color: self.model_space.selection_crossing_color,
|
||||||
|
highlight_color: self.model_space.selection_highlight_color,
|
||||||
|
grip_size: self.model_space.grip_size as f32,
|
||||||
|
grip_color: self.model_space.grip_color,
|
||||||
|
grip_hot: self.model_space.grip_hot,
|
||||||
|
grip_hover: self.model_space.grip_hover,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -803,18 +806,23 @@ impl OpenCADStudio {
|
|||||||
.on_scroll(Message::ViewportScroll)
|
.on_scroll(Message::ViewportScroll)
|
||||||
.on_exit(Message::ViewportExit);
|
.on_exit(Message::ViewportExit);
|
||||||
|
|
||||||
|
let desk_bg = self.model_space.resolve_desk_bg();
|
||||||
|
let desk_color = Color {
|
||||||
|
r: desk_bg[0],
|
||||||
|
g: desk_bg[1],
|
||||||
|
b: desk_bg[2],
|
||||||
|
a: desk_bg[3],
|
||||||
|
};
|
||||||
let bg_color = if is_paper {
|
let bg_color = if is_paper {
|
||||||
PAPER_SPACE_BACKGROUND
|
desk_color
|
||||||
} else {
|
} else {
|
||||||
tab.bg_color
|
let c = tab.scene.bg_color;
|
||||||
.map(|[r, g, b, a]| Color { r, g, b, a })
|
Color {
|
||||||
.unwrap_or(Color {
|
r: c[0],
|
||||||
// Default model background: RGB (33, 40, 48).
|
g: c[1],
|
||||||
r: 33.0 / 255.0,
|
b: c[2],
|
||||||
g: 40.0 / 255.0,
|
a: c[3],
|
||||||
b: 48.0 / 255.0,
|
}
|
||||||
a: 1.0,
|
|
||||||
})
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Dynamic input overlay — editable boxes near the cursor, one per
|
// Dynamic input overlay — editable boxes near the cursor, one per
|
||||||
@@ -944,7 +952,7 @@ impl OpenCADStudio {
|
|||||||
.width(Fill)
|
.width(Fill)
|
||||||
.height(Fill)
|
.height(Fill)
|
||||||
} else if thumbnail_capture_clean {
|
} else if thumbnail_capture_clean {
|
||||||
let capture_bg = if is_paper { PAPER_SPACE_BACKGROUND } else { bg_color };
|
let capture_bg = if is_paper { desk_color } else { bg_color };
|
||||||
stack![
|
stack![
|
||||||
container(Space::new())
|
container(Space::new())
|
||||||
.style(move |_: &Theme| container::Style {
|
.style(move |_: &Theme| container::Style {
|
||||||
@@ -962,7 +970,7 @@ impl OpenCADStudio {
|
|||||||
stack![
|
stack![
|
||||||
container(Space::new())
|
container(Space::new())
|
||||||
.style(move |_: &Theme| container::Style {
|
.style(move |_: &Theme| container::Style {
|
||||||
background: Some(Background::Color(PAPER_SPACE_BACKGROUND)),
|
background: Some(Background::Color(desk_color)),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
.width(Fill)
|
.width(Fill)
|
||||||
|
|||||||
@@ -174,8 +174,8 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
super::super::ModalKind::Options => sized_flow(
|
super::super::ModalKind::Options => sized_flow(
|
||||||
ex,
|
ex,
|
||||||
520,
|
540,
|
||||||
500,
|
560,
|
||||||
|flow| {
|
|flow| {
|
||||||
crate::ui::window::options::view_window(
|
crate::ui::window::options::view_window(
|
||||||
&self.default_save_format,
|
&self.default_save_format,
|
||||||
@@ -190,6 +190,10 @@ impl OpenCADStudio {
|
|||||||
self.crosshair_color,
|
self.crosshair_color,
|
||||||
&self.crosshair_color_input,
|
&self.crosshair_color_input,
|
||||||
self.lineweight_display_scale,
|
self.lineweight_display_scale,
|
||||||
|
&self.model_space,
|
||||||
|
&self.model_bg_input,
|
||||||
|
&self.paper_bg_input,
|
||||||
|
&self.desk_bg_input,
|
||||||
flow,
|
flow,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1466,6 +1466,10 @@ pub struct Scene {
|
|||||||
/// Entity drawn with the selection-highlight colour without being part
|
/// Entity drawn with the selection-highlight colour without being part
|
||||||
/// of the real selection — used to preview a row in the cycling list box.
|
/// of the real selection — used to preview a row in the cycling list box.
|
||||||
pub hover_highlight: Option<Handle>,
|
pub hover_highlight: Option<Handle>,
|
||||||
|
/// Color used to draw the selection highlight overlay wires (SELECTIONEFFECTCOLOR).
|
||||||
|
pub selection_color: [f32; 4],
|
||||||
|
/// Whether selection visual effect (glow/highlight) is enabled (SELECTIONEFFECT).
|
||||||
|
pub selection_effect: bool,
|
||||||
/// Whether entity transparency is honoured on screen. When false the
|
/// Whether entity transparency is honoured on screen. When false the
|
||||||
/// wire shader forces every line opaque (a uniform toggle, no retessellate).
|
/// wire shader forces every line opaque (a uniform toggle, no retessellate).
|
||||||
pub transparency_display: bool,
|
pub transparency_display: bool,
|
||||||
@@ -1882,6 +1886,8 @@ impl Scene {
|
|||||||
command_preview_hidden: HashSet::default(),
|
command_preview_hidden: HashSet::default(),
|
||||||
refedit_keep: None,
|
refedit_keep: None,
|
||||||
hover_highlight: None,
|
hover_highlight: None,
|
||||||
|
selection_color: crate::scene::model::wire_model::WireModel::SELECTED,
|
||||||
|
selection_effect: true,
|
||||||
transparency_display: true,
|
transparency_display: true,
|
||||||
model_lineweight_scale: 1.0,
|
model_lineweight_scale: 1.0,
|
||||||
selection_filter: HashSet::default(),
|
selection_filter: HashSet::default(),
|
||||||
|
|||||||
+53
-35
@@ -2430,6 +2430,7 @@ impl Pipeline {
|
|||||||
hovered: &rustc_hash::FxHashSet<acadrust::Handle>,
|
hovered: &rustc_hash::FxHashSet<acadrust::Handle>,
|
||||||
annotation_context_wires: &[WireModel],
|
annotation_context_wires: &[WireModel],
|
||||||
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
|
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
|
||||||
|
selected_tint: Option<[f32; 4]>,
|
||||||
) {
|
) {
|
||||||
let perf_started = crate::perf::enabled().then(iced::time::Instant::now);
|
let perf_started = crate::perf::enabled().then(iced::time::Instant::now);
|
||||||
if selected.is_empty() && hovered.is_empty() && annotation_context_wires.is_empty() {
|
if selected.is_empty() && hovered.is_empty() && annotation_context_wires.is_empty() {
|
||||||
@@ -2490,13 +2491,17 @@ impl Pipeline {
|
|||||||
.copied()
|
.copied()
|
||||||
.filter(|wire| wire.render_instance.is_some())
|
.filter(|wire| wire.render_instance.is_some())
|
||||||
.collect();
|
.collect();
|
||||||
let mut gpu = WireGpu::from_highlight_refs(
|
let mut gpu = if let Some(tint) = selected_tint {
|
||||||
device,
|
WireGpu::from_highlight_refs(
|
||||||
&selected_regular,
|
device,
|
||||||
WireModel::SELECTED,
|
&selected_regular,
|
||||||
depth_map,
|
tint,
|
||||||
self.wire_const_bgl.as_ref(),
|
depth_map,
|
||||||
);
|
self.wire_const_bgl.as_ref(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
gpu.extend(WireGpu::from_highlight_refs(
|
gpu.extend(WireGpu::from_highlight_refs(
|
||||||
device,
|
device,
|
||||||
&hover_regular,
|
&hover_regular,
|
||||||
@@ -2505,13 +2510,17 @@ impl Pipeline {
|
|||||||
self.wire_const_bgl.as_ref(),
|
self.wire_const_bgl.as_ref(),
|
||||||
));
|
));
|
||||||
self.gpu_selected_wires = gpu;
|
self.gpu_selected_wires = gpu;
|
||||||
let mut block_gpu = BlockWireGpu::from_wires(
|
let mut block_gpu = if let Some(tint) = selected_tint {
|
||||||
device,
|
BlockWireGpu::from_wires(
|
||||||
&selected_blocks,
|
device,
|
||||||
depth_map,
|
&selected_blocks,
|
||||||
Some(WireModel::SELECTED),
|
depth_map,
|
||||||
&self.block_wire_const_bgl,
|
Some(tint),
|
||||||
);
|
&self.block_wire_const_bgl,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
block_gpu.extend(BlockWireGpu::from_wires(
|
block_gpu.extend(BlockWireGpu::from_wires(
|
||||||
device,
|
device,
|
||||||
&hover_blocks,
|
&hover_blocks,
|
||||||
@@ -2549,6 +2558,7 @@ impl Pipeline {
|
|||||||
hovered: &rustc_hash::FxHashSet<acadrust::Handle>,
|
hovered: &rustc_hash::FxHashSet<acadrust::Handle>,
|
||||||
annotation_context_wires: &[WireModel],
|
annotation_context_wires: &[WireModel],
|
||||||
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
|
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
|
||||||
|
selected_tint: Option<[f32; 4]>,
|
||||||
) {
|
) {
|
||||||
let perf_started = crate::perf::enabled().then(iced::time::Instant::now);
|
let perf_started = crate::perf::enabled().then(iced::time::Instant::now);
|
||||||
if selected.is_empty() && hovered.is_empty() && annotation_context_wires.is_empty() {
|
if selected.is_empty() && hovered.is_empty() && annotation_context_wires.is_empty() {
|
||||||
@@ -2585,15 +2595,17 @@ impl Pipeline {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for h in selected {
|
if let Some(tint) = selected_tint {
|
||||||
push(
|
for h in selected {
|
||||||
&self.wire_handle_index,
|
push(
|
||||||
h.value(),
|
&self.wire_handle_index,
|
||||||
WireModel::SELECTED,
|
h.value(),
|
||||||
wires,
|
tint,
|
||||||
&mut out,
|
wires,
|
||||||
&mut selected_blocks,
|
&mut out,
|
||||||
);
|
&mut selected_blocks,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for h in hovered.iter().filter(|handle| !selected.contains(handle)) {
|
for h in hovered.iter().filter(|handle| !selected.contains(handle)) {
|
||||||
push(
|
push(
|
||||||
@@ -2607,25 +2619,31 @@ impl Pipeline {
|
|||||||
}
|
}
|
||||||
for wire in annotation_context_wires {
|
for wire in annotation_context_wires {
|
||||||
let tint = if wire.selected {
|
let tint = if wire.selected {
|
||||||
WireModel::SELECTED
|
selected_tint.unwrap_or(WireModel::SELECTED)
|
||||||
} else {
|
} else {
|
||||||
WireModel::HOVER
|
WireModel::HOVER
|
||||||
};
|
};
|
||||||
for vertex in &wire.text_verts {
|
if !wire.selected || selected_tint.is_some() {
|
||||||
out.push(text_gpu::TextVertex {
|
for vertex in &wire.text_verts {
|
||||||
color: [tint[0], tint[1], tint[2], vertex.color[3]],
|
out.push(text_gpu::TextVertex {
|
||||||
..*vertex
|
color: [tint[0], tint[1], tint[2], vertex.color[3]],
|
||||||
});
|
..*vertex
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.text_highlight_vcount = out.len() as u32;
|
self.text_highlight_vcount = out.len() as u32;
|
||||||
self.text_highlight_vbuf = text_gpu::upload_vertices(device, &out);
|
self.text_highlight_vbuf = text_gpu::upload_vertices(device, &out);
|
||||||
let mut block_gpu = text_gpu::upload_block_vertex_refs(
|
let mut block_gpu = if let Some(tint) = selected_tint {
|
||||||
device,
|
text_gpu::upload_block_vertex_refs(
|
||||||
&selected_blocks,
|
device,
|
||||||
depth_map,
|
&selected_blocks,
|
||||||
Some(WireModel::SELECTED),
|
depth_map,
|
||||||
);
|
Some(tint),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
block_gpu.extend(text_gpu::upload_block_vertex_refs(
|
block_gpu.extend(text_gpu::upload_block_vertex_refs(
|
||||||
device,
|
device,
|
||||||
&hover_blocks,
|
&hover_blocks,
|
||||||
|
|||||||
@@ -255,6 +255,10 @@ pub struct Primitive {
|
|||||||
pub(in crate::scene) bg_color: [f32; 4],
|
pub(in crate::scene) bg_color: [f32; 4],
|
||||||
/// Active Iced theme text colour for GPU-rendered ViewCube labels.
|
/// Active Iced theme text colour for GPU-rendered ViewCube labels.
|
||||||
pub(in crate::scene) viewcube_text_color: [f32; 4],
|
pub(in crate::scene) viewcube_text_color: [f32; 4],
|
||||||
|
/// Color used to draw the selection highlight overlay wires.
|
||||||
|
pub(in crate::scene) selection_color: [f32; 4],
|
||||||
|
/// Whether selection visual effect (glow/highlight) is enabled.
|
||||||
|
pub(in crate::scene) selection_effect: bool,
|
||||||
/// One input-to-render sample, carried only when PERF tracing is enabled.
|
/// One input-to-render sample, carried only when PERF tracing is enabled.
|
||||||
pub(in crate::scene) nav_perf: Option<NavPerfSample>,
|
pub(in crate::scene) nav_perf: Option<NavPerfSample>,
|
||||||
}
|
}
|
||||||
@@ -989,6 +993,11 @@ impl shader::Primitive for Primitive {
|
|||||||
&& !Arc::ptr_eq(previous, &vp.annotation_context_wires)
|
&& !Arc::ptr_eq(previous, &vp.annotation_context_wires)
|
||||||
});
|
});
|
||||||
if selection_changed || highlighted_geometry_changed || annotation_context_changed {
|
if selection_changed || highlighted_geometry_changed || annotation_context_changed {
|
||||||
|
let sel_color = if self.selection_effect {
|
||||||
|
Some(self.selection_color)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
inner.upload_selected_wires(
|
inner.upload_selected_wires(
|
||||||
device,
|
device,
|
||||||
&vp_wires[..],
|
&vp_wires[..],
|
||||||
@@ -996,6 +1005,7 @@ impl shader::Primitive for Primitive {
|
|||||||
&vp.hover_handles,
|
&vp.hover_handles,
|
||||||
&vp.annotation_context_wires,
|
&vp.annotation_context_wires,
|
||||||
&draw_depths,
|
&draw_depths,
|
||||||
|
sel_color,
|
||||||
);
|
);
|
||||||
// Text highlight rides the same selection key: a pick / rollover
|
// Text highlight rides the same selection key: a pick / rollover
|
||||||
// recolours the selected / hovered glyphs without touching the
|
// recolours the selected / hovered glyphs without touching the
|
||||||
@@ -1007,6 +1017,7 @@ impl shader::Primitive for Primitive {
|
|||||||
&vp.hover_handles,
|
&vp.hover_handles,
|
||||||
&vp.annotation_context_wires,
|
&vp.annotation_context_wires,
|
||||||
&draw_depths,
|
&draw_depths,
|
||||||
|
sel_color,
|
||||||
);
|
);
|
||||||
inner.cached_annotation_highlight_source =
|
inner.cached_annotation_highlight_source =
|
||||||
Some(Arc::clone(&vp.annotation_context_wires));
|
Some(Arc::clone(&vp.annotation_context_wires));
|
||||||
@@ -3339,7 +3350,7 @@ impl Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let tint = if selected {
|
let tint = if selected {
|
||||||
WireModel::SELECTED
|
self.selection_color
|
||||||
} else {
|
} else {
|
||||||
WireModel::HOVER
|
WireModel::HOVER
|
||||||
};
|
};
|
||||||
@@ -3387,7 +3398,7 @@ impl Scene {
|
|||||||
entity,
|
entity,
|
||||||
content_viewport.then_some(inst.handle),
|
content_viewport.then_some(inst.handle),
|
||||||
);
|
);
|
||||||
wire.color = WireModel::SELECTED;
|
wire.color = self.selection_color;
|
||||||
wire.selected = true;
|
wire.selected = true;
|
||||||
wire.pattern_length = pattern_length;
|
wire.pattern_length = pattern_length;
|
||||||
wire.pattern = pattern;
|
wire.pattern = pattern;
|
||||||
@@ -3527,6 +3538,8 @@ impl Scene {
|
|||||||
viewports,
|
viewports,
|
||||||
bg_color,
|
bg_color,
|
||||||
viewcube_text_color,
|
viewcube_text_color,
|
||||||
|
selection_color: self.selection_color,
|
||||||
|
selection_effect: self.selection_effect,
|
||||||
nav_perf: perf_nav,
|
nav_perf: perf_nav,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3555,6 +3568,8 @@ impl Scene {
|
|||||||
viewports: vec![],
|
viewports: vec![],
|
||||||
bg_color,
|
bg_color,
|
||||||
viewcube_text_color,
|
viewcube_text_color,
|
||||||
|
selection_color: self.selection_color,
|
||||||
|
selection_effect: self.selection_effect,
|
||||||
nav_perf: None,
|
nav_perf: None,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -3615,6 +3630,8 @@ impl Scene {
|
|||||||
viewports,
|
viewports,
|
||||||
bg_color,
|
bg_color,
|
||||||
viewcube_text_color,
|
viewcube_text_color,
|
||||||
|
selection_color: self.selection_color,
|
||||||
|
selection_effect: self.selection_effect,
|
||||||
nav_perf: perf_nav,
|
nav_perf: perf_nav,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+244
-79
@@ -71,6 +71,52 @@ pub struct CrosshairOptions {
|
|||||||
pub snap_angle_deg: f32,
|
pub snap_angle_deg: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Rendering style for the viewport grid.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct GridStyle {
|
||||||
|
pub opacity: u8,
|
||||||
|
pub bg_luminance: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for GridStyle {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
opacity: 18,
|
||||||
|
bg_luminance: 0.15,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Visual effect options for selection windows, crossings, entity highlights, and grips.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct SelectionVisualOptions {
|
||||||
|
pub area: bool,
|
||||||
|
pub opacity: u8,
|
||||||
|
pub window_color: u8,
|
||||||
|
pub crossing_color: u8,
|
||||||
|
pub highlight_color: u8,
|
||||||
|
pub grip_size: f32,
|
||||||
|
pub grip_color: u8,
|
||||||
|
pub grip_hot: u8,
|
||||||
|
pub grip_hover: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SelectionVisualOptions {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
area: true,
|
||||||
|
opacity: 12,
|
||||||
|
window_color: 0,
|
||||||
|
crossing_color: 0,
|
||||||
|
highlight_color: 0,
|
||||||
|
grip_size: 5.0,
|
||||||
|
grip_color: 0,
|
||||||
|
grip_hot: 0,
|
||||||
|
grip_hover: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Grip marker data ──────────────────────────────────────────────────────
|
// ── Grip marker data ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Describes one grip to be drawn in the viewport overlay.
|
/// Describes one grip to be drawn in the viewport overlay.
|
||||||
@@ -147,13 +193,13 @@ impl GridGeometry {
|
|||||||
pub(crate) struct GridKey {
|
pub(crate) struct GridKey {
|
||||||
pub grids: Vec<GridParams>,
|
pub grids: Vec<GridParams>,
|
||||||
pub bounds: iced::Rectangle,
|
pub bounds: iced::Rectangle,
|
||||||
|
pub style: GridStyle,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GridKey {
|
impl GridKey {
|
||||||
/// Build a key from the per-pane `GridParams` and the overlay bounds. The
|
/// Build a key from the per-pane `GridParams`, overlay bounds, and grid style.
|
||||||
/// input slice is copied; callers can drop the original.
|
pub(crate) fn from_grids(grids: &[GridParams], bounds: iced::Rectangle, style: GridStyle) -> Self {
|
||||||
pub(crate) fn from_grids(grids: &[GridParams], bounds: iced::Rectangle) -> Self {
|
Self { grids: grids.to_vec(), bounds, style }
|
||||||
Self { grids: grids.to_vec(), bounds }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,8 +327,9 @@ pub struct OstTrackPoint {
|
|||||||
|
|
||||||
pub fn grid_overlay<'a>(
|
pub fn grid_overlay<'a>(
|
||||||
grid: Vec<GridParams>,
|
grid: Vec<GridParams>,
|
||||||
|
style: GridStyle,
|
||||||
) -> Element<'a, Message> {
|
) -> Element<'a, Message> {
|
||||||
canvas(GridCanvas { grid })
|
canvas(GridCanvas { grid, style })
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.height(Length::Fill)
|
.height(Length::Fill)
|
||||||
.into()
|
.into()
|
||||||
@@ -290,6 +337,7 @@ pub fn grid_overlay<'a>(
|
|||||||
|
|
||||||
struct GridCanvas {
|
struct GridCanvas {
|
||||||
grid: Vec<GridParams>,
|
grid: Vec<GridParams>,
|
||||||
|
style: GridStyle,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl canvas::Program<Message> for GridCanvas {
|
impl canvas::Program<Message> for GridCanvas {
|
||||||
@@ -303,10 +351,10 @@ impl canvas::Program<Message> for GridCanvas {
|
|||||||
bounds: iced::Rectangle,
|
bounds: iced::Rectangle,
|
||||||
_cursor: mouse::Cursor,
|
_cursor: mouse::Cursor,
|
||||||
) -> Vec<canvas::Geometry> {
|
) -> Vec<canvas::Geometry> {
|
||||||
let key = GridKey::from_grids(&self.grid, bounds);
|
let key = GridKey::from_grids(&self.grid, bounds, self.style);
|
||||||
|
|
||||||
// Hit check: same params, same bounds ⇒ the cached geometry is still
|
// Hit check: same params, same bounds, same style ⇒ the cached geometry is still
|
||||||
// valid. The key includes bounds, so a `should_reuse` match implies
|
// valid. The key includes bounds and style, so a `should_reuse` match implies
|
||||||
// both are equal, and the fork's `draw_with_bounds` will return the
|
// both are equal, and the fork's `draw_with_bounds` will return the
|
||||||
// cached `Arc` clone (essentially free).
|
// cached `Arc` clone (essentially free).
|
||||||
let hit = should_reuse(state.key.borrow().as_ref(), &key);
|
let hit = should_reuse(state.key.borrow().as_ref(), &key);
|
||||||
@@ -316,11 +364,8 @@ impl canvas::Program<Message> for GridCanvas {
|
|||||||
// returns the cached geometry without invoking the closure.
|
// returns the cached geometry without invoking the closure.
|
||||||
state.cache.draw_with_bounds(renderer, bounds, |_frame| {})
|
state.cache.draw_with_bounds(renderer, bounds, |_frame| {})
|
||||||
} else {
|
} else {
|
||||||
// Params or bounds changed. Clear so the closure runs even when
|
// Params, style, or bounds changed. Clear so the closure runs even when
|
||||||
// bounds happen to match the previously-cached frame (e.g. a pan
|
// bounds happen to match the previously-cached frame.
|
||||||
// within the same canvas size would otherwise leave stale
|
|
||||||
// geometry served — the fork's bounds-equality test would return
|
|
||||||
// the cached clone without calling our rebuild closure).
|
|
||||||
state.cache.clear();
|
state.cache.clear();
|
||||||
state.cache.draw_with_bounds(renderer, bounds, |frame| {
|
state.cache.draw_with_bounds(renderer, bounds, |frame| {
|
||||||
for g in &self.grid {
|
for g in &self.grid {
|
||||||
@@ -348,14 +393,14 @@ impl canvas::Program<Message> for GridCanvas {
|
|||||||
g.origin,
|
g.origin,
|
||||||
g.axes,
|
g.axes,
|
||||||
g.limits,
|
g.limits,
|
||||||
|
self.style,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update the stored key. Safe: the `borrow()` for `should_reuse` is
|
// Update the stored key.
|
||||||
// dropped at the end of the `if` expression above; no live Ref here.
|
|
||||||
*state.key.borrow_mut() = Some(key);
|
*state.key.borrow_mut() = Some(key);
|
||||||
|
|
||||||
vec![geometry]
|
vec![geometry]
|
||||||
@@ -383,6 +428,7 @@ pub fn selection_overlay<'a>(
|
|||||||
hover_locked: bool,
|
hover_locked: bool,
|
||||||
crosshair_bg: [f32; 4],
|
crosshair_bg: [f32; 4],
|
||||||
crosshair: CrosshairOptions,
|
crosshair: CrosshairOptions,
|
||||||
|
selection_visual: SelectionVisualOptions,
|
||||||
) -> Element<'a, Message> {
|
) -> Element<'a, Message> {
|
||||||
canvas(SelectionCanvas {
|
canvas(SelectionCanvas {
|
||||||
selection,
|
selection,
|
||||||
@@ -405,6 +451,7 @@ pub fn selection_overlay<'a>(
|
|||||||
hover_locked,
|
hover_locked,
|
||||||
crosshair_bg,
|
crosshair_bg,
|
||||||
crosshair,
|
crosshair,
|
||||||
|
selection_visual,
|
||||||
})
|
})
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.height(Length::Fill)
|
.height(Length::Fill)
|
||||||
@@ -462,11 +509,17 @@ struct SelectionCanvas {
|
|||||||
/// rather than the UI theme, which may be light over a dark model viewport.
|
/// rather than the UI theme, which may be light over a dark model viewport.
|
||||||
crosshair_bg: [f32; 4],
|
crosshair_bg: [f32; 4],
|
||||||
crosshair: CrosshairOptions,
|
crosshair: CrosshairOptions,
|
||||||
|
selection_visual: SelectionVisualOptions,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_grip_marker(frame: &mut canvas::Frame, grip: &GripMarker, theme: &Theme) {
|
fn draw_grip_marker(
|
||||||
|
frame: &mut canvas::Frame,
|
||||||
|
grip: &GripMarker,
|
||||||
|
theme: &Theme,
|
||||||
|
visual: &SelectionVisualOptions,
|
||||||
|
) {
|
||||||
let sp = grip.pos;
|
let sp = grip.pos;
|
||||||
let h = crate::scene::pick::grip::GRIP_HALF_PX;
|
let h = visual.grip_size.clamp(1.0, 25.0);
|
||||||
let path = match grip.shape {
|
let path = match grip.shape {
|
||||||
GripShape::Square => canvas::Path::rectangle(
|
GripShape::Square => canvas::Path::rectangle(
|
||||||
Point::new(sp.x - h, sp.y - h),
|
Point::new(sp.x - h, sp.y - h),
|
||||||
@@ -521,10 +574,28 @@ fn draw_grip_marker(frame: &mut canvas::Frame, grip: &GripMarker, theme: &Theme)
|
|||||||
};
|
};
|
||||||
|
|
||||||
if grip.is_hot {
|
if grip.is_hot {
|
||||||
frame.fill(&path, theme.palette().danger.base.color);
|
let hot_color = if visual.grip_hot > 0 {
|
||||||
|
if let Some((r, g, b)) = acadrust::types::aci_table::aci_to_rgb(visual.grip_hot) {
|
||||||
|
Color::from_rgb8(r, g, b)
|
||||||
|
} else {
|
||||||
|
theme.palette().danger.base.color
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
theme.palette().danger.base.color
|
||||||
|
};
|
||||||
|
frame.fill(&path, hot_color);
|
||||||
} else if grip.is_hovered {
|
} else if grip.is_hovered {
|
||||||
let pair = theme.palette().primary.strong;
|
let pair = theme.palette().primary.strong;
|
||||||
frame.fill(&path, pair.color);
|
let hover_color = if visual.grip_hover > 0 {
|
||||||
|
if let Some((r, g, b)) = acadrust::types::aci_table::aci_to_rgb(visual.grip_hover) {
|
||||||
|
Color::from_rgb8(r, g, b)
|
||||||
|
} else {
|
||||||
|
pair.color
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pair.color
|
||||||
|
};
|
||||||
|
frame.fill(&path, hover_color);
|
||||||
frame.stroke(
|
frame.stroke(
|
||||||
&path,
|
&path,
|
||||||
canvas::Stroke {
|
canvas::Stroke {
|
||||||
@@ -535,7 +606,15 @@ fn draw_grip_marker(frame: &mut canvas::Frame, grip: &GripMarker, theme: &Theme)
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
let palette = theme.palette();
|
let palette = theme.palette();
|
||||||
let color = palette.primary.base.color;
|
let color = if visual.grip_color > 0 {
|
||||||
|
if let Some((r, g, b)) = acadrust::types::aci_table::aci_to_rgb(visual.grip_color) {
|
||||||
|
Color::from_rgb8(r, g, b)
|
||||||
|
} else {
|
||||||
|
palette.primary.base.color
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
palette.primary.base.color
|
||||||
|
};
|
||||||
let fill = if grip.shape == GripShape::Dropdown {
|
let fill = if grip.shape == GripShape::Dropdown {
|
||||||
color
|
color
|
||||||
} else {
|
} else {
|
||||||
@@ -715,20 +794,40 @@ impl canvas::Program<Message> for SelectionCanvas {
|
|||||||
b: Point,
|
b: Point,
|
||||||
crossing: bool,
|
crossing: bool,
|
||||||
theme: &Theme,
|
theme: &Theme,
|
||||||
|
visual: &SelectionVisualOptions,
|
||||||
) {
|
) {
|
||||||
let base = if crossing {
|
let base = if crossing {
|
||||||
theme.palette().success.base.color
|
if visual.crossing_color > 0 {
|
||||||
|
if let Some((r, g, b)) = acadrust::types::aci_table::aci_to_rgb(visual.crossing_color) {
|
||||||
|
Color::from_rgb8(r, g, b)
|
||||||
|
} else {
|
||||||
|
theme.palette().success.base.color
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
theme.palette().success.base.color
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
theme.palette().primary.base.color
|
if visual.window_color > 0 {
|
||||||
|
if let Some((r, g, b)) = acadrust::types::aci_table::aci_to_rgb(visual.window_color) {
|
||||||
|
Color::from_rgb8(r, g, b)
|
||||||
|
} else {
|
||||||
|
theme.palette().primary.base.color
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
theme.palette().primary.base.color
|
||||||
|
}
|
||||||
};
|
};
|
||||||
let fill = base.scale_alpha(0.12);
|
|
||||||
let stroke = base.scale_alpha(0.9);
|
|
||||||
let x0 = a.x.min(b.x);
|
let x0 = a.x.min(b.x);
|
||||||
let y0 = a.y.min(b.y);
|
let y0 = a.y.min(b.y);
|
||||||
let w = (a.x - b.x).abs();
|
let w = (a.x - b.x).abs();
|
||||||
let h = (a.y - b.y).abs();
|
let h = (a.y - b.y).abs();
|
||||||
let rect = canvas::Path::rectangle(Point::new(x0, y0), Size::new(w, h));
|
let rect = canvas::Path::rectangle(Point::new(x0, y0), Size::new(w, h));
|
||||||
frame.fill(&rect, fill);
|
if visual.area && visual.opacity > 0 {
|
||||||
|
let alpha = (visual.opacity as f32 / 100.0).clamp(0.0, 1.0);
|
||||||
|
let fill = base.scale_alpha(alpha);
|
||||||
|
frame.fill(&rect, fill);
|
||||||
|
}
|
||||||
|
let stroke = base.scale_alpha(0.9);
|
||||||
frame.stroke(
|
frame.stroke(
|
||||||
&rect,
|
&rect,
|
||||||
canvas::Stroke {
|
canvas::Stroke {
|
||||||
@@ -740,33 +839,52 @@ impl canvas::Program<Message> for SelectionCanvas {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let (Some(a), Some(b)) = (self.selection.borrow().box_anchor, self.selection.borrow().box_current) {
|
if let (Some(a), Some(b)) = (self.selection.borrow().box_anchor, self.selection.borrow().box_current) {
|
||||||
draw_marquee(&mut frame, a, b, self.selection.borrow().box_crossing, theme);
|
draw_marquee(&mut frame, a, b, self.selection.borrow().box_crossing, theme, &self.selection_visual);
|
||||||
}
|
}
|
||||||
// Preview marquee for point-picked windows (STRETCH) — same look, no pick.
|
// Preview marquee for point-picked windows (STRETCH) — same look, no pick.
|
||||||
if let Some((a, b, crossing)) = self.selection.borrow().preview_box {
|
if let Some((a, b, crossing)) = self.selection.borrow().preview_box {
|
||||||
draw_marquee(&mut frame, a, b, crossing, theme);
|
draw_marquee(&mut frame, a, b, crossing, theme, &self.selection_visual);
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.selection.borrow().poly_active && self.selection.borrow().poly_points.len() > 1 {
|
if self.selection.borrow().poly_active && self.selection.borrow().poly_points.len() > 1 {
|
||||||
let base = if self.selection.borrow().poly_crossing {
|
let base = if self.selection.borrow().poly_crossing {
|
||||||
theme.palette().success.base.color
|
if self.selection_visual.crossing_color > 0 {
|
||||||
} else {
|
if let Some((r, g, b)) = acadrust::types::aci_table::aci_to_rgb(self.selection_visual.crossing_color) {
|
||||||
theme.palette().primary.base.color
|
Color::from_rgb8(r, g, b)
|
||||||
};
|
} else {
|
||||||
let fill = base.scale_alpha(0.12);
|
theme.palette().success.base.color
|
||||||
let stroke = base.scale_alpha(0.9);
|
|
||||||
if let Some(cur) = self.selection.borrow().last_move_pos {
|
|
||||||
let start = self.selection.borrow().poly_points[0];
|
|
||||||
let fill_path = canvas::Path::new(|p| {
|
|
||||||
p.move_to(start);
|
|
||||||
for pt in &self.selection.borrow().poly_points[1..] {
|
|
||||||
p.line_to(*pt);
|
|
||||||
}
|
}
|
||||||
p.line_to(cur);
|
} else {
|
||||||
p.line_to(start);
|
theme.palette().success.base.color
|
||||||
});
|
}
|
||||||
frame.fill(&fill_path, fill);
|
} else {
|
||||||
|
if self.selection_visual.window_color > 0 {
|
||||||
|
if let Some((r, g, b)) = acadrust::types::aci_table::aci_to_rgb(self.selection_visual.window_color) {
|
||||||
|
Color::from_rgb8(r, g, b)
|
||||||
|
} else {
|
||||||
|
theme.palette().primary.base.color
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
theme.palette().primary.base.color
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if self.selection_visual.area && self.selection_visual.opacity > 0 {
|
||||||
|
let alpha = (self.selection_visual.opacity as f32 / 100.0).clamp(0.0, 1.0);
|
||||||
|
let fill = base.scale_alpha(alpha);
|
||||||
|
if let Some(cur) = self.selection.borrow().last_move_pos {
|
||||||
|
let start = self.selection.borrow().poly_points[0];
|
||||||
|
let fill_path = canvas::Path::new(|p| {
|
||||||
|
p.move_to(start);
|
||||||
|
for pt in &self.selection.borrow().poly_points[1..] {
|
||||||
|
p.line_to(*pt);
|
||||||
|
}
|
||||||
|
p.line_to(cur);
|
||||||
|
p.line_to(start);
|
||||||
|
});
|
||||||
|
frame.fill(&fill_path, fill);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
let stroke = base.scale_alpha(0.9);
|
||||||
let path = canvas::Path::new(|p| {
|
let path = canvas::Path::new(|p| {
|
||||||
p.move_to(self.selection.borrow().poly_points[0]);
|
p.move_to(self.selection.borrow().poly_points[0]);
|
||||||
for pt in &self.selection.borrow().poly_points[1..] {
|
for pt in &self.selection.borrow().poly_points[1..] {
|
||||||
@@ -840,7 +958,7 @@ impl canvas::Program<Message> for SelectionCanvas {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for grip in &self.grips {
|
for grip in &self.grips {
|
||||||
draw_grip_marker(frame, grip, theme);
|
draw_grip_marker(frame, grip, theme, &self.selection_visual);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1242,6 +1360,7 @@ impl canvas::Program<Message> for SelectionCanvas {
|
|||||||
ucs.origin_screen,
|
ucs.origin_screen,
|
||||||
ucs.hover,
|
ucs.hover,
|
||||||
ucs.selected,
|
ucs.selected,
|
||||||
|
self.crosshair_bg,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1341,6 +1460,7 @@ const MIN_GRID_PX: f32 = 20.0;
|
|||||||
/// Stop an infinite perspective grid before adjacent lines merge at the horizon.
|
/// Stop an infinite perspective grid before adjacent lines merge at the horizon.
|
||||||
const MIN_HORIZON_GRID_PX: f32 = 5.0;
|
const MIN_HORIZON_GRID_PX: f32 = 5.0;
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn draw_grid(
|
fn draw_grid(
|
||||||
frame: &mut canvas::Frame,
|
frame: &mut canvas::Frame,
|
||||||
view_rot: Mat4,
|
view_rot: Mat4,
|
||||||
@@ -1350,12 +1470,25 @@ fn draw_grid(
|
|||||||
grid_origin: glam::DVec3,
|
grid_origin: glam::DVec3,
|
||||||
grid_axes: (Vec3, Vec3, Vec3),
|
grid_axes: (Vec3, Vec3, Vec3),
|
||||||
limits: Option<(glam::DVec2, glam::DVec2)>,
|
limits: Option<(glam::DVec2, glam::DVec2)>,
|
||||||
|
style: GridStyle,
|
||||||
) {
|
) {
|
||||||
let gc = Color {
|
let alpha = (style.opacity as f32 / 100.0).clamp(0.02, 1.0);
|
||||||
r: 0.28,
|
let gc = if style.bg_luminance > 0.5 {
|
||||||
g: 0.28,
|
// Light background: subtle dark grid lines
|
||||||
b: 0.28,
|
Color {
|
||||||
a: 0.7,
|
r: 0.10,
|
||||||
|
g: 0.10,
|
||||||
|
b: 0.10,
|
||||||
|
a: alpha,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Dark background: subtle light grid lines
|
||||||
|
Color {
|
||||||
|
r: 0.80,
|
||||||
|
g: 0.80,
|
||||||
|
b: 0.80,
|
||||||
|
a: alpha,
|
||||||
|
}
|
||||||
};
|
};
|
||||||
let st = canvas::Stroke {
|
let st = canvas::Stroke {
|
||||||
width: 0.5,
|
width: 0.5,
|
||||||
@@ -1375,7 +1508,7 @@ fn draw_grid(
|
|||||||
if geometry.axis_extent > 0.0 {
|
if geometry.axis_extent > 0.0 {
|
||||||
let (gx, gy, gz) = grid_axes;
|
let (gx, gy, gz) = grid_axes;
|
||||||
let extent = (geometry.axis_extent + step) * 1.5;
|
let extent = (geometry.axis_extent + step) * 1.5;
|
||||||
draw_axes(frame, view_rot, eye, bounds, extent.max(10.0), grid_origin, (gx, gy, gz));
|
draw_axes(frame, view_rot, eye, bounds, extent.max(10.0), grid_origin, (gx, gy, gz), style.bg_luminance);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1922,6 +2055,7 @@ pub(crate) fn grid_segments(
|
|||||||
|
|
||||||
// ── Coloured UCS axes ──────────────────────────────────────────────────────
|
// ── Coloured UCS axes ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn draw_axes(
|
fn draw_axes(
|
||||||
frame: &mut canvas::Frame,
|
frame: &mut canvas::Frame,
|
||||||
view_rot: Mat4,
|
view_rot: Mat4,
|
||||||
@@ -1930,6 +2064,7 @@ fn draw_axes(
|
|||||||
extent: f32,
|
extent: f32,
|
||||||
origin: glam::DVec3,
|
origin: glam::DVec3,
|
||||||
axes: (Vec3, Vec3, Vec3),
|
axes: (Vec3, Vec3, Vec3),
|
||||||
|
bg_luminance: f32,
|
||||||
) {
|
) {
|
||||||
let w2s = |world: glam::DVec3| -> Point {
|
let w2s = |world: glam::DVec3| -> Point {
|
||||||
let ndc = view_rot.project_point3((world - eye).as_vec3());
|
let ndc = view_rot.project_point3((world - eye).as_vec3());
|
||||||
@@ -1955,8 +2090,9 @@ fn draw_axes(
|
|||||||
axis_stroke(r, g, b),
|
axis_stroke(r, g, b),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
let y_green = if bg_luminance > 0.5 { 0.60 } else { 0.85 };
|
||||||
line(ax, 0.90, 0.20, 0.20); // X — red
|
line(ax, 0.90, 0.20, 0.20); // X — red
|
||||||
line(ay, 0.20, 0.85, 0.20); // Y — green
|
line(ay, 0.20, y_green, 0.20); // Y — green
|
||||||
line(az, 0.20, 0.40, 0.90); // Z — blue
|
line(az, 0.20, 0.40, 0.90); // Z — blue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2090,6 +2226,7 @@ fn draw_ucs_icon(
|
|||||||
origin_screen: Option<Point>,
|
origin_screen: Option<Point>,
|
||||||
hover: bool,
|
hover: bool,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
|
crosshair_bg: [f32; 4],
|
||||||
) {
|
) {
|
||||||
let Some((icon_origin, at_origin, geom)) =
|
let Some((icon_origin, at_origin, geom)) =
|
||||||
ucs_icon_geometry(vp, bounds, axes, origin_screen)
|
ucs_icon_geometry(vp, bounds, axes, origin_screen)
|
||||||
@@ -2101,6 +2238,8 @@ fn draw_ucs_icon(
|
|||||||
// corner), so hover/selection highlight applies there too.
|
// corner), so hover/selection highlight applies there too.
|
||||||
let _ = at_origin;
|
let _ = at_origin;
|
||||||
let highlight = hover || selected;
|
let highlight = hover || selected;
|
||||||
|
let is_light_bg = 0.299 * crosshair_bg[0] + 0.587 * crosshair_bg[1] + 0.114 * crosshair_bg[2] > 0.5;
|
||||||
|
let y_green = if is_light_bg { 0.60 } else { 0.85 };
|
||||||
|
|
||||||
struct AxisInfo {
|
struct AxisInfo {
|
||||||
dx: f32,
|
dx: f32,
|
||||||
@@ -2129,7 +2268,7 @@ fn draw_ucs_icon(
|
|||||||
sc_len: geom[1].sc_len,
|
sc_len: geom[1].sc_len,
|
||||||
depth: geom[1].depth,
|
depth: geom[1].depth,
|
||||||
r: 0.22,
|
r: 0.22,
|
||||||
g: 0.85,
|
g: y_green,
|
||||||
b: 0.22,
|
b: 0.22,
|
||||||
label: "Y",
|
label: "Y",
|
||||||
},
|
},
|
||||||
@@ -2228,15 +2367,23 @@ fn draw_ucs_icon(
|
|||||||
|
|
||||||
// Origin dot.
|
// Origin dot.
|
||||||
let circle = canvas::Path::circle(icon_origin, 3.5);
|
let circle = canvas::Path::circle(icon_origin, 3.5);
|
||||||
frame.fill(
|
let dot_lum = 0.299 * crosshair_bg[0] + 0.587 * crosshair_bg[1] + 0.114 * crosshair_bg[2];
|
||||||
&circle,
|
let dot_color = if dot_lum > 0.5 {
|
||||||
|
Color {
|
||||||
|
r: 0.15,
|
||||||
|
g: 0.15,
|
||||||
|
b: 0.15,
|
||||||
|
a: 0.95,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
Color {
|
Color {
|
||||||
r: 0.9,
|
r: 0.9,
|
||||||
g: 0.9,
|
g: 0.9,
|
||||||
b: 0.9,
|
b: 0.9,
|
||||||
a: 0.95,
|
a: 0.95,
|
||||||
},
|
}
|
||||||
);
|
};
|
||||||
|
frame.fill(&circle, dot_color);
|
||||||
|
|
||||||
// Draggable grips when selected: a square at the origin and at the X / Y
|
// Draggable grips when selected: a square at the origin and at the X / Y
|
||||||
// tips. Warm grip colour with a light border, like an entity grip.
|
// tips. Warm grip colour with a light border, like an entity grip.
|
||||||
@@ -3043,11 +3190,11 @@ mod bench_grid_geometry_tests {
|
|||||||
// Pre-seed a `GridCanvasState` with the same key the bench will
|
// Pre-seed a `GridCanvasState` with the same key the bench will
|
||||||
// build each iteration — guaranteed hit path.
|
// build each iteration — guaranteed hit path.
|
||||||
let state = GridCanvasState::default();
|
let state = GridCanvasState::default();
|
||||||
let stored_key = GridKey::from_grids(&grids, canvas_bounds);
|
let stored_key = GridKey::from_grids(&grids, canvas_bounds, GridStyle::default());
|
||||||
*state.key.borrow_mut() = Some(stored_key);
|
*state.key.borrow_mut() = Some(stored_key);
|
||||||
|
|
||||||
for _ in 0..20 {
|
for _ in 0..20 {
|
||||||
let key = GridKey::from_grids(black_box(&grids), black_box(canvas_bounds));
|
let key = GridKey::from_grids(black_box(&grids), black_box(canvas_bounds), GridStyle::default());
|
||||||
let hit = should_reuse(state.key.borrow().as_ref(), &key);
|
let hit = should_reuse(state.key.borrow().as_ref(), &key);
|
||||||
black_box(hit);
|
black_box(hit);
|
||||||
}
|
}
|
||||||
@@ -3056,7 +3203,7 @@ mod bench_grid_geometry_tests {
|
|||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let mut hit_count = 0u32;
|
let mut hit_count = 0u32;
|
||||||
for _ in 0..n {
|
for _ in 0..n {
|
||||||
let key = GridKey::from_grids(black_box(&grids), black_box(canvas_bounds));
|
let key = GridKey::from_grids(black_box(&grids), black_box(canvas_bounds), GridStyle::default());
|
||||||
if should_reuse(state.key.borrow().as_ref(), &key) {
|
if should_reuse(state.key.borrow().as_ref(), &key) {
|
||||||
hit_count += 1;
|
hit_count += 1;
|
||||||
}
|
}
|
||||||
@@ -3098,17 +3245,17 @@ mod grid_key_tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Same `Vec<GridParams>` + same bounds ⇒ keys compare equal.
|
/// Same `Vec<GridParams>` + same bounds + same style ⇒ keys compare equal.
|
||||||
#[test]
|
#[test]
|
||||||
fn grid_key_matches_identical_params() {
|
fn grid_key_matches_identical_params() {
|
||||||
let grids = vec![baseline_params(), baseline_params()];
|
let grids = vec![baseline_params(), baseline_params()];
|
||||||
let bounds = iced::Rectangle { x: 0.0, y: 0.0, width: 1920.0, height: 720.0 };
|
let bounds = iced::Rectangle { x: 0.0, y: 0.0, width: 1920.0, height: 720.0 };
|
||||||
let a = GridKey::from_grids(&grids, bounds);
|
let a = GridKey::from_grids(&grids, bounds, GridStyle::default());
|
||||||
let b = GridKey::from_grids(&grids, bounds);
|
let b = GridKey::from_grids(&grids, bounds, GridStyle::default());
|
||||||
assert_eq!(a, b);
|
assert_eq!(a, b);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One test, one baseline. Every change of any of the 7 inputs must produce
|
/// One test, one baseline. Every change of any of the inputs must produce
|
||||||
/// a key that differs from the baseline. This is the entire correctness
|
/// a key that differs from the baseline. This is the entire correctness
|
||||||
/// contract for cache hit/miss — if any input is ignored, the cache serves
|
/// contract for cache hit/miss — if any input is ignored, the cache serves
|
||||||
/// a stale grid.
|
/// a stale grid.
|
||||||
@@ -3121,13 +3268,13 @@ mod grid_key_tests {
|
|||||||
height: 720.0,
|
height: 720.0,
|
||||||
};
|
};
|
||||||
let baseline_grids = vec![baseline_params()];
|
let baseline_grids = vec![baseline_params()];
|
||||||
let baseline_key = GridKey::from_grids(&baseline_grids, baseline_bounds);
|
let baseline_key = GridKey::from_grids(&baseline_grids, baseline_bounds, GridStyle::default());
|
||||||
|
|
||||||
// view_rot: small extra rotation
|
// view_rot: small extra rotation
|
||||||
let mut p = baseline_params();
|
let mut p = baseline_params();
|
||||||
p.view_rot = Mat4::from_rotation_x(0.15 + 0.01) * Mat4::from_rotation_y(0.05);
|
p.view_rot = Mat4::from_rotation_x(0.15 + 0.01) * Mat4::from_rotation_y(0.05);
|
||||||
assert_ne!(
|
assert_ne!(
|
||||||
GridKey::from_grids(&[p], baseline_bounds),
|
GridKey::from_grids(&[p], baseline_bounds, GridStyle::default()),
|
||||||
baseline_key,
|
baseline_key,
|
||||||
"view_rot change must invalidate"
|
"view_rot change must invalidate"
|
||||||
);
|
);
|
||||||
@@ -3136,7 +3283,7 @@ mod grid_key_tests {
|
|||||||
let mut p = baseline_params();
|
let mut p = baseline_params();
|
||||||
p.eye = glam::DVec3::new(4.0, 3.5, 9.5);
|
p.eye = glam::DVec3::new(4.0, 3.5, 9.5);
|
||||||
assert_ne!(
|
assert_ne!(
|
||||||
GridKey::from_grids(&[p], baseline_bounds),
|
GridKey::from_grids(&[p], baseline_bounds, GridStyle::default()),
|
||||||
baseline_key,
|
baseline_key,
|
||||||
"eye change must invalidate"
|
"eye change must invalidate"
|
||||||
);
|
);
|
||||||
@@ -3145,7 +3292,7 @@ mod grid_key_tests {
|
|||||||
let mut p = baseline_params();
|
let mut p = baseline_params();
|
||||||
p.step = 40.0;
|
p.step = 40.0;
|
||||||
assert_ne!(
|
assert_ne!(
|
||||||
GridKey::from_grids(&[p], baseline_bounds),
|
GridKey::from_grids(&[p], baseline_bounds, GridStyle::default()),
|
||||||
baseline_key,
|
baseline_key,
|
||||||
"step change must invalidate"
|
"step change must invalidate"
|
||||||
);
|
);
|
||||||
@@ -3154,7 +3301,7 @@ mod grid_key_tests {
|
|||||||
let mut p = baseline_params();
|
let mut p = baseline_params();
|
||||||
p.origin = glam::DVec3::new(100.0, 0.0, 0.0);
|
p.origin = glam::DVec3::new(100.0, 0.0, 0.0);
|
||||||
assert_ne!(
|
assert_ne!(
|
||||||
GridKey::from_grids(&[p], baseline_bounds),
|
GridKey::from_grids(&[p], baseline_bounds, GridStyle::default()),
|
||||||
baseline_key,
|
baseline_key,
|
||||||
"origin change must invalidate"
|
"origin change must invalidate"
|
||||||
);
|
);
|
||||||
@@ -3163,7 +3310,7 @@ mod grid_key_tests {
|
|||||||
let mut p = baseline_params();
|
let mut p = baseline_params();
|
||||||
p.axes = (Vec3::Y, Vec3::X, Vec3::Z);
|
p.axes = (Vec3::Y, Vec3::X, Vec3::Z);
|
||||||
assert_ne!(
|
assert_ne!(
|
||||||
GridKey::from_grids(&[p], baseline_bounds),
|
GridKey::from_grids(&[p], baseline_bounds, GridStyle::default()),
|
||||||
baseline_key,
|
baseline_key,
|
||||||
"axes change must invalidate"
|
"axes change must invalidate"
|
||||||
);
|
);
|
||||||
@@ -3172,7 +3319,7 @@ mod grid_key_tests {
|
|||||||
let mut p = baseline_params();
|
let mut p = baseline_params();
|
||||||
p.limits = Some((glam::DVec2::new(0.0, 0.0), glam::DVec2::new(100.0, 100.0)));
|
p.limits = Some((glam::DVec2::new(0.0, 0.0), glam::DVec2::new(100.0, 100.0)));
|
||||||
assert_ne!(
|
assert_ne!(
|
||||||
GridKey::from_grids(&[p], baseline_bounds),
|
GridKey::from_grids(&[p], baseline_bounds, GridStyle::default()),
|
||||||
baseline_key,
|
baseline_key,
|
||||||
"limits change must invalidate"
|
"limits change must invalidate"
|
||||||
);
|
);
|
||||||
@@ -3185,10 +3332,28 @@ mod grid_key_tests {
|
|||||||
height: 720.0,
|
height: 720.0,
|
||||||
};
|
};
|
||||||
assert_ne!(
|
assert_ne!(
|
||||||
GridKey::from_grids(&baseline_grids, other_bounds),
|
GridKey::from_grids(&baseline_grids, other_bounds, GridStyle::default()),
|
||||||
baseline_key,
|
baseline_key,
|
||||||
"bounds change must invalidate"
|
"bounds change must invalidate"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// style opacity: change opacity
|
||||||
|
let mut style = GridStyle::default();
|
||||||
|
style.opacity = 50;
|
||||||
|
assert_ne!(
|
||||||
|
GridKey::from_grids(&baseline_grids, baseline_bounds, style),
|
||||||
|
baseline_key,
|
||||||
|
"style opacity change must invalidate"
|
||||||
|
);
|
||||||
|
|
||||||
|
// style bg_luminance: change luminance (dark to light)
|
||||||
|
let mut style = GridStyle::default();
|
||||||
|
style.bg_luminance = 0.9;
|
||||||
|
assert_ne!(
|
||||||
|
GridKey::from_grids(&baseline_grids, baseline_bounds, style),
|
||||||
|
baseline_key,
|
||||||
|
"style bg_luminance change must invalidate"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// In a 2-pane tiled layout, changing the second pane's params (with pane 1
|
/// In a 2-pane tiled layout, changing the second pane's params (with pane 1
|
||||||
@@ -3200,13 +3365,13 @@ mod grid_key_tests {
|
|||||||
let pane1 = baseline_params();
|
let pane1 = baseline_params();
|
||||||
let pane2 = baseline_params();
|
let pane2 = baseline_params();
|
||||||
let both = vec![pane1.clone(), pane2.clone()];
|
let both = vec![pane1.clone(), pane2.clone()];
|
||||||
let baseline = GridKey::from_grids(&both, bounds);
|
let baseline = GridKey::from_grids(&both, bounds, GridStyle::default());
|
||||||
|
|
||||||
let mut pane2_changed = pane2;
|
let mut pane2_changed = pane2;
|
||||||
pane2_changed.step = 160.0;
|
pane2_changed.step = 160.0;
|
||||||
let dirty = vec![pane1, pane2_changed];
|
let dirty = vec![pane1, pane2_changed];
|
||||||
assert_ne!(
|
assert_ne!(
|
||||||
GridKey::from_grids(&dirty, bounds),
|
GridKey::from_grids(&dirty, bounds, GridStyle::default()),
|
||||||
baseline,
|
baseline,
|
||||||
"second pane change must invalidate"
|
"second pane change must invalidate"
|
||||||
);
|
);
|
||||||
@@ -3217,7 +3382,7 @@ mod grid_key_tests {
|
|||||||
fn should_reuse_empty() {
|
fn should_reuse_empty() {
|
||||||
let grids = vec![baseline_params()];
|
let grids = vec![baseline_params()];
|
||||||
let bounds = iced::Rectangle { x: 0.0, y: 0.0, width: 1920.0, height: 720.0 };
|
let bounds = iced::Rectangle { x: 0.0, y: 0.0, width: 1920.0, height: 720.0 };
|
||||||
let key = GridKey::from_grids(&grids, bounds);
|
let key = GridKey::from_grids(&grids, bounds, GridStyle::default());
|
||||||
assert!(!should_reuse(None, &key));
|
assert!(!should_reuse(None, &key));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3226,7 +3391,7 @@ mod grid_key_tests {
|
|||||||
fn should_reuse_equal() {
|
fn should_reuse_equal() {
|
||||||
let grids = vec![baseline_params()];
|
let grids = vec![baseline_params()];
|
||||||
let bounds = iced::Rectangle { x: 0.0, y: 0.0, width: 1920.0, height: 720.0 };
|
let bounds = iced::Rectangle { x: 0.0, y: 0.0, width: 1920.0, height: 720.0 };
|
||||||
let key = GridKey::from_grids(&grids, bounds);
|
let key = GridKey::from_grids(&grids, bounds, GridStyle::default());
|
||||||
assert!(should_reuse(Some(&key), &key));
|
assert!(should_reuse(Some(&key), &key));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3239,8 +3404,8 @@ mod grid_key_tests {
|
|||||||
pane2.step = 160.0;
|
pane2.step = 160.0;
|
||||||
let grids_b = vec![pane2];
|
let grids_b = vec![pane2];
|
||||||
let bounds = iced::Rectangle { x: 0.0, y: 0.0, width: 1920.0, height: 720.0 };
|
let bounds = iced::Rectangle { x: 0.0, y: 0.0, width: 1920.0, height: 720.0 };
|
||||||
let old = GridKey::from_grids(&grids_a, bounds);
|
let old = GridKey::from_grids(&grids_a, bounds, GridStyle::default());
|
||||||
let new = GridKey::from_grids(&grids_b, bounds);
|
let new = GridKey::from_grids(&grids_b, bounds, GridStyle::default());
|
||||||
assert!(!should_reuse(Some(&old), &new));
|
assert!(!should_reuse(Some(&old), &new));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3281,7 +3446,7 @@ mod grid_canvas_state_tests {
|
|||||||
axes: grid_axes,
|
axes: grid_axes,
|
||||||
limits,
|
limits,
|
||||||
};
|
};
|
||||||
let key = GridKey::from_grids(&[params], bounds);
|
let key = GridKey::from_grids(&[params], bounds, GridStyle::default());
|
||||||
|
|
||||||
let state = GridCanvasState::default();
|
let state = GridCanvasState::default();
|
||||||
*state.key.borrow_mut() = Some(key.clone());
|
*state.key.borrow_mut() = Some(key.clone());
|
||||||
@@ -3291,7 +3456,7 @@ mod grid_canvas_state_tests {
|
|||||||
|
|
||||||
// Different bounds on the same params ⇒ different key, do not reuse.
|
// Different bounds on the same params ⇒ different key, do not reuse.
|
||||||
let other_bounds = iced::Rectangle { x: 0.0, y: 0.0, width: 640.0, height: 480.0 };
|
let other_bounds = iced::Rectangle { x: 0.0, y: 0.0, width: 640.0, height: 480.0 };
|
||||||
let other_key = GridKey::from_grids(&[params], other_bounds);
|
let other_key = GridKey::from_grids(&[params], other_bounds, GridStyle::default());
|
||||||
assert!(!should_reuse(state.key.borrow().as_ref(), &other_key));
|
assert!(!should_reuse(state.key.borrow().as_ref(), &other_key));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+429
-44
@@ -27,6 +27,7 @@ impl<T> fmt::Display for Labelled<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn view_window<'a>(
|
pub fn view_window<'a>(
|
||||||
default_save_format: &'a str,
|
default_save_format: &'a str,
|
||||||
file_assoc_enabled: bool,
|
file_assoc_enabled: bool,
|
||||||
@@ -40,6 +41,10 @@ pub fn view_window<'a>(
|
|||||||
crosshair_color: Option<[u8; 3]>,
|
crosshair_color: Option<[u8; 3]>,
|
||||||
crosshair_color_input: &'a str,
|
crosshair_color_input: &'a str,
|
||||||
lineweight_display_scale: i32,
|
lineweight_display_scale: i32,
|
||||||
|
model_space: &'a crate::app::config::ModelSpaceThemeConfig,
|
||||||
|
model_bg_input: &'a str,
|
||||||
|
paper_bg_input: &'a str,
|
||||||
|
desk_bg_input: &'a str,
|
||||||
sizing: crate::ui::modal::ModalSizing,
|
sizing: crate::ui::modal::ModalSizing,
|
||||||
) -> Element<'a, Message> {
|
) -> Element<'a, Message> {
|
||||||
let selected_format = crate::io::SAVE_FORMAT_OPTIONS
|
let selected_format = crate::io::SAVE_FORMAT_OPTIONS
|
||||||
@@ -204,7 +209,144 @@ pub fn view_window<'a>(
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
|
|
||||||
let display = column![
|
let model_bg_rgb = model_space.custom_bg.unwrap_or(crate::app::config::CLASSIC_CAD_DARK_BG);
|
||||||
|
let model_bg_swatch = container(Space::new())
|
||||||
|
.width(28)
|
||||||
|
.height(22)
|
||||||
|
.style(move |theme: &Theme| container::Style {
|
||||||
|
background: Some(Background::Color(iced::Color::from_rgb8(
|
||||||
|
model_bg_rgb[0],
|
||||||
|
model_bg_rgb[1],
|
||||||
|
model_bg_rgb[2],
|
||||||
|
))),
|
||||||
|
border: Border {
|
||||||
|
color: theme.palette().background.strong.color,
|
||||||
|
width: 1.0,
|
||||||
|
radius: 3.0.into(),
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
let paper_bg_rgb = model_space.custom_paper_bg.unwrap_or(crate::app::config::DEFAULT_PAPER_BG);
|
||||||
|
let paper_bg_swatch = container(Space::new())
|
||||||
|
.width(28)
|
||||||
|
.height(22)
|
||||||
|
.style(move |theme: &Theme| container::Style {
|
||||||
|
background: Some(Background::Color(iced::Color::from_rgb8(
|
||||||
|
paper_bg_rgb[0],
|
||||||
|
paper_bg_rgb[1],
|
||||||
|
paper_bg_rgb[2],
|
||||||
|
))),
|
||||||
|
border: Border {
|
||||||
|
color: theme.palette().background.strong.color,
|
||||||
|
width: 1.0,
|
||||||
|
radius: 3.0.into(),
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
let desk_bg_rgb = model_space.custom_desk_bg.unwrap_or(crate::app::config::DEFAULT_DESK_BG);
|
||||||
|
let desk_bg_swatch = container(Space::new())
|
||||||
|
.width(28)
|
||||||
|
.height(22)
|
||||||
|
.style(move |theme: &Theme| container::Style {
|
||||||
|
background: Some(Background::Color(iced::Color::from_rgb8(
|
||||||
|
desk_bg_rgb[0],
|
||||||
|
desk_bg_rgb[1],
|
||||||
|
desk_bg_rgb[2],
|
||||||
|
))),
|
||||||
|
border: Border {
|
||||||
|
color: theme.palette().background.strong.color,
|
||||||
|
width: 1.0,
|
||||||
|
radius: 3.0.into(),
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
let mode_options = crate::app::config::ModelSpaceMode::ALL
|
||||||
|
.into_iter()
|
||||||
|
.map(|value| Labelled {
|
||||||
|
label: crate::t!(value.label()).into_owned(),
|
||||||
|
value,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let selected_mode = mode_options
|
||||||
|
.iter()
|
||||||
|
.find(|choice| choice.value == model_space.mode)
|
||||||
|
.cloned();
|
||||||
|
|
||||||
|
let color_choice_options = [
|
||||||
|
(0u8, "0: Theme Default"),
|
||||||
|
(1, "1: Red"),
|
||||||
|
(2, "2: Yellow"),
|
||||||
|
(3, "3: Green"),
|
||||||
|
(4, "4: Cyan"),
|
||||||
|
(5, "5: Blue"),
|
||||||
|
(6, "6: Magenta"),
|
||||||
|
(7, "7: White/Black"),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.map(|(value, label)| Labelled {
|
||||||
|
value,
|
||||||
|
label: label.to_string(),
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let selected_window_color = color_choice_options
|
||||||
|
.iter()
|
||||||
|
.find(|c| c.value == model_space.selection_window_color)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| Labelled {
|
||||||
|
value: model_space.selection_window_color,
|
||||||
|
label: format!("ACI {}", model_space.selection_window_color),
|
||||||
|
});
|
||||||
|
|
||||||
|
let selected_crossing_color = color_choice_options
|
||||||
|
.iter()
|
||||||
|
.find(|c| c.value == model_space.selection_crossing_color)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| Labelled {
|
||||||
|
value: model_space.selection_crossing_color,
|
||||||
|
label: format!("ACI {}", model_space.selection_crossing_color),
|
||||||
|
});
|
||||||
|
|
||||||
|
let selected_highlight_color = color_choice_options
|
||||||
|
.iter()
|
||||||
|
.find(|c| c.value == model_space.selection_highlight_color)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| Labelled {
|
||||||
|
value: model_space.selection_highlight_color,
|
||||||
|
label: format!("ACI {}", model_space.selection_highlight_color),
|
||||||
|
});
|
||||||
|
|
||||||
|
let selected_grip_color = color_choice_options
|
||||||
|
.iter()
|
||||||
|
.find(|c| c.value == model_space.grip_color)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| Labelled {
|
||||||
|
value: model_space.grip_color,
|
||||||
|
label: format!("ACI {}", model_space.grip_color),
|
||||||
|
});
|
||||||
|
|
||||||
|
let selected_grip_hot = color_choice_options
|
||||||
|
.iter()
|
||||||
|
.find(|c| c.value == model_space.grip_hot)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| Labelled {
|
||||||
|
value: model_space.grip_hot,
|
||||||
|
label: format!("ACI {}", model_space.grip_hot),
|
||||||
|
});
|
||||||
|
|
||||||
|
let selected_grip_hover = color_choice_options
|
||||||
|
.iter()
|
||||||
|
.find(|c| c.value == model_space.grip_hover)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| Labelled {
|
||||||
|
value: model_space.grip_hover,
|
||||||
|
label: format!("ACI {}", model_space.grip_hover),
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut display = column![
|
||||||
text(crate::tr!("options", "theme-section")).size(15),
|
text(crate::tr!("options", "theme-section")).size(15),
|
||||||
Space::new().height(10),
|
Space::new().height(10),
|
||||||
row![
|
row![
|
||||||
@@ -226,71 +368,176 @@ pub fn view_window<'a>(
|
|||||||
Space::new().height(12),
|
Space::new().height(12),
|
||||||
color_controls,
|
color_controls,
|
||||||
Space::new().height(24),
|
Space::new().height(24),
|
||||||
text(crate::t!("Crosshair")).size(15),
|
|
||||||
Space::new().height(10),
|
|
||||||
row![
|
row![
|
||||||
text(crate::t!("Crosshair size")).size(12).width(140),
|
text(crate::t!("Model Space Appearance")).size(15),
|
||||||
slider(1..=100, cursor_size.clamp(1, 100), Message::CursorSizeChanged)
|
Space::new().width(Fill),
|
||||||
.step(1)
|
button(text(crate::t!("Restore Defaults")).size(11))
|
||||||
.width(Fill),
|
.on_press(Message::RestoreModelSpaceDisplayDefaults)
|
||||||
text(format!("{}%", cursor_size.clamp(1, 100)))
|
.padding([4, 10])
|
||||||
.size(11)
|
.style(button::secondary),
|
||||||
.width(44),
|
|
||||||
]
|
]
|
||||||
.spacing(10)
|
|
||||||
.align_y(iced::Center),
|
.align_y(iced::Center),
|
||||||
Space::new().height(10),
|
Space::new().height(10),
|
||||||
row![
|
row![
|
||||||
text(crate::t!("Cursor type")).size(12).width(140),
|
text(crate::t!("Canvas mode")).size(12).width(140),
|
||||||
iced::widget::pick_list(
|
iced::widget::pick_list(
|
||||||
selected_cursor,
|
selected_mode,
|
||||||
cursor_options,
|
mode_options,
|
||||||
|choice| choice.label.clone(),
|
|choice| choice.label.clone(),
|
||||||
)
|
)
|
||||||
.on_select(|choice| Message::CursorTypeChanged(choice.value))
|
.on_select(|choice| Message::ModelSpaceModeChanged(choice.value))
|
||||||
.width(Fill),
|
.width(Fill),
|
||||||
]
|
]
|
||||||
.spacing(10)
|
.spacing(10)
|
||||||
.align_y(iced::Center),
|
.align_y(iced::Center),
|
||||||
Space::new().height(10),
|
];
|
||||||
|
|
||||||
|
if model_space.mode == crate::app::config::ModelSpaceMode::Custom {
|
||||||
|
display = display.push(Space::new().height(10)).push(
|
||||||
|
row![
|
||||||
|
text(crate::t!("Model background")).size(12).width(140),
|
||||||
|
model_bg_swatch,
|
||||||
|
text_input("#RRGGBB", model_bg_input)
|
||||||
|
.on_input(Message::ModelSpaceBgChanged)
|
||||||
|
.width(150),
|
||||||
|
]
|
||||||
|
.spacing(10)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
display = display.push(Space::new().height(10)).push(
|
||||||
row![
|
row![
|
||||||
text(crate::t!("Crosshair color")).size(12).width(140),
|
text(crate::t!("Paper background")).size(12).width(140),
|
||||||
crosshair_swatch,
|
paper_bg_swatch,
|
||||||
text_input(crate::t!("#RRGGBB or blank").as_ref(), crosshair_color_input)
|
text_input("#RRGGBB", paper_bg_input)
|
||||||
.on_input(Message::CrosshairColorChanged)
|
.on_input(Message::PaperSpaceBgChanged)
|
||||||
.width(150),
|
.width(150),
|
||||||
]
|
]
|
||||||
.spacing(10)
|
.spacing(10)
|
||||||
.align_y(iced::Center),
|
.align_y(iced::Center),
|
||||||
Space::new().height(6),
|
);
|
||||||
text(crate::t!("Leave the color blank to keep automatic viewport contrast."))
|
|
||||||
.size(11)
|
display = display.push(Space::new().height(10)).push(
|
||||||
.width(sizing.width),
|
|
||||||
Space::new().height(24),
|
|
||||||
text(crate::t!("Lineweight")).size(15),
|
|
||||||
Space::new().height(10),
|
|
||||||
row![
|
row![
|
||||||
text(crate::t!("Model display scale")).size(12).width(140),
|
text(crate::t!("Desk surround")).size(12).width(140),
|
||||||
slider(
|
desk_bg_swatch,
|
||||||
25..=200,
|
text_input("#RRGGBB", desk_bg_input)
|
||||||
lineweight_display_scale.clamp(25, 200),
|
.on_input(Message::DeskSpaceBgChanged)
|
||||||
Message::LineweightDisplayScaleChanged,
|
.width(150),
|
||||||
)
|
|
||||||
.step(1)
|
|
||||||
.width(Fill),
|
|
||||||
text(format!("{}%", lineweight_display_scale.clamp(25, 200)))
|
|
||||||
.size(11)
|
|
||||||
.width(44),
|
|
||||||
]
|
]
|
||||||
.spacing(10)
|
.spacing(10)
|
||||||
.align_y(iced::Center),
|
.align_y(iced::Center),
|
||||||
Space::new().height(6),
|
);
|
||||||
text(crate::t!("Changes the on-screen width in Model without affecting plotted output."))
|
|
||||||
|
display = display
|
||||||
|
.push(Space::new().height(10))
|
||||||
|
.push(
|
||||||
|
row![
|
||||||
|
text(crate::t!("Grid opacity")).size(12).width(140),
|
||||||
|
slider(5..=100, model_space.grid_opacity.clamp(5, 100), Message::GridOpacityChanged)
|
||||||
|
.step(1)
|
||||||
|
.width(Fill),
|
||||||
|
text(format!("{}%", model_space.grid_opacity.clamp(5, 100)))
|
||||||
|
.size(11)
|
||||||
|
.width(44),
|
||||||
|
]
|
||||||
|
.spacing(10)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
)
|
||||||
|
.push(Space::new().height(6))
|
||||||
|
.push(
|
||||||
|
text(match model_space.mode {
|
||||||
|
crate::app::config::ModelSpaceMode::MatchTheme => {
|
||||||
|
crate::t!("Canvas background, grid, and default line colors automatically adapt to the active theme.")
|
||||||
|
}
|
||||||
|
crate::app::config::ModelSpaceMode::ClassicDark => {
|
||||||
|
crate::t!("Model space canvas remains locked to classic CAD dark charcoal (#212830).")
|
||||||
|
}
|
||||||
|
crate::app::config::ModelSpaceMode::Custom => {
|
||||||
|
crate::t!("Custom background colors set above are applied to Model and Paper space.")
|
||||||
|
}
|
||||||
|
})
|
||||||
.size(11)
|
.size(11)
|
||||||
.width(sizing.width),
|
.width(sizing.width),
|
||||||
]
|
)
|
||||||
.spacing(0)
|
.push(Space::new().height(24))
|
||||||
.width(sizing.width);
|
.push(text(crate::t!("Crosshair")).size(15))
|
||||||
|
.push(Space::new().height(10))
|
||||||
|
.push(
|
||||||
|
row![
|
||||||
|
text(crate::t!("Crosshair size")).size(12).width(140),
|
||||||
|
slider(1..=100, cursor_size.clamp(1, 100), Message::CursorSizeChanged)
|
||||||
|
.step(1)
|
||||||
|
.width(Fill),
|
||||||
|
text(format!("{}%", cursor_size.clamp(1, 100)))
|
||||||
|
.size(11)
|
||||||
|
.width(44),
|
||||||
|
]
|
||||||
|
.spacing(10)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
)
|
||||||
|
.push(Space::new().height(10))
|
||||||
|
.push(
|
||||||
|
row![
|
||||||
|
text(crate::t!("Cursor type")).size(12).width(140),
|
||||||
|
iced::widget::pick_list(
|
||||||
|
selected_cursor,
|
||||||
|
cursor_options,
|
||||||
|
|choice| choice.label.clone(),
|
||||||
|
)
|
||||||
|
.on_select(|choice| Message::CursorTypeChanged(choice.value))
|
||||||
|
.width(Fill),
|
||||||
|
]
|
||||||
|
.spacing(10)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
)
|
||||||
|
.push(Space::new().height(10))
|
||||||
|
.push(
|
||||||
|
row![
|
||||||
|
text(crate::t!("Crosshair color")).size(12).width(140),
|
||||||
|
crosshair_swatch,
|
||||||
|
text_input(crate::t!("#RRGGBB or blank").as_ref(), crosshair_color_input)
|
||||||
|
.on_input(Message::CrosshairColorChanged)
|
||||||
|
.width(150),
|
||||||
|
]
|
||||||
|
.spacing(10)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
)
|
||||||
|
.push(Space::new().height(6))
|
||||||
|
.push(
|
||||||
|
text(crate::t!("Leave the color blank to keep automatic viewport contrast."))
|
||||||
|
.size(11)
|
||||||
|
.width(sizing.width),
|
||||||
|
)
|
||||||
|
.push(Space::new().height(24))
|
||||||
|
.push(text(crate::t!("Lineweight")).size(15))
|
||||||
|
.push(Space::new().height(10))
|
||||||
|
.push(
|
||||||
|
row![
|
||||||
|
text(crate::t!("Model display scale")).size(12).width(140),
|
||||||
|
slider(
|
||||||
|
25..=200,
|
||||||
|
lineweight_display_scale.clamp(25, 200),
|
||||||
|
Message::LineweightDisplayScaleChanged,
|
||||||
|
)
|
||||||
|
.step(1)
|
||||||
|
.width(Fill),
|
||||||
|
text(format!("{}%", lineweight_display_scale.clamp(25, 200)))
|
||||||
|
.size(11)
|
||||||
|
.width(44),
|
||||||
|
]
|
||||||
|
.spacing(10)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
)
|
||||||
|
.push(Space::new().height(6))
|
||||||
|
.push(
|
||||||
|
text(crate::t!("Changes the on-screen width in Model without affecting plotted output."))
|
||||||
|
.size(11)
|
||||||
|
.width(sizing.width),
|
||||||
|
);
|
||||||
|
|
||||||
|
let display_element = display.spacing(0).width(sizing.width);
|
||||||
|
|
||||||
let selection = column![
|
let selection = column![
|
||||||
text(crate::t!("Selection")).size(15),
|
text(crate::t!("Selection")).size(15),
|
||||||
@@ -310,13 +557,151 @@ pub fn view_window<'a>(
|
|||||||
))
|
))
|
||||||
.size(11)
|
.size(11)
|
||||||
.width(sizing.width),
|
.width(sizing.width),
|
||||||
|
Space::new().height(24),
|
||||||
|
row![
|
||||||
|
text(crate::t!("Visual Effect Settings")).size(15),
|
||||||
|
Space::new().width(Fill),
|
||||||
|
button(text(crate::t!("Restore Defaults")).size(11))
|
||||||
|
.on_press(Message::RestoreSelectionVisualDefaults)
|
||||||
|
.padding([4, 10])
|
||||||
|
.style(button::secondary),
|
||||||
|
]
|
||||||
|
.align_y(iced::Center),
|
||||||
|
Space::new().height(10),
|
||||||
|
row![
|
||||||
|
iced::widget::checkbox(model_space.selection_area)
|
||||||
|
.on_toggle(Message::SelectionAreaToggled)
|
||||||
|
.size(15),
|
||||||
|
text(crate::t!("Indicate selection area with transparent fill (SELECTIONAREA)"))
|
||||||
|
.size(12),
|
||||||
|
]
|
||||||
|
.spacing(8)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
Space::new().height(10),
|
||||||
|
row![
|
||||||
|
text(crate::t!("Selection area opacity")).size(12).width(140),
|
||||||
|
slider(
|
||||||
|
0..=100,
|
||||||
|
model_space.selection_opacity.clamp(0, 100),
|
||||||
|
Message::SelectionOpacityChanged,
|
||||||
|
)
|
||||||
|
.step(1)
|
||||||
|
.width(Fill),
|
||||||
|
text(format!("{}%", model_space.selection_opacity.clamp(0, 100)))
|
||||||
|
.size(11)
|
||||||
|
.width(44),
|
||||||
|
]
|
||||||
|
.spacing(10)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
Space::new().height(6),
|
||||||
|
text(crate::t!(
|
||||||
|
"Transparency of the window and crossing selection areas (SELECTIONAREAOPACITY)."
|
||||||
|
))
|
||||||
|
.size(11)
|
||||||
|
.width(sizing.width),
|
||||||
|
Space::new().height(10),
|
||||||
|
row![
|
||||||
|
text(crate::t!("Window selection color")).size(12).width(140),
|
||||||
|
iced::widget::pick_list(
|
||||||
|
Some(selected_window_color),
|
||||||
|
color_choice_options.clone(),
|
||||||
|
|choice| choice.label.clone(),
|
||||||
|
)
|
||||||
|
.on_select(|choice| Message::SelectionWindowColorChanged(choice.value))
|
||||||
|
.width(Fill),
|
||||||
|
]
|
||||||
|
.spacing(10)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
Space::new().height(10),
|
||||||
|
row![
|
||||||
|
text(crate::t!("Crossing selection color")).size(12).width(140),
|
||||||
|
iced::widget::pick_list(
|
||||||
|
Some(selected_crossing_color),
|
||||||
|
color_choice_options.clone(),
|
||||||
|
|choice| choice.label.clone(),
|
||||||
|
)
|
||||||
|
.on_select(|choice| Message::SelectionCrossingColorChanged(choice.value))
|
||||||
|
.width(Fill),
|
||||||
|
]
|
||||||
|
.spacing(10)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
Space::new().height(10),
|
||||||
|
row![
|
||||||
|
text(crate::t!("Selection highlight color")).size(12).width(140),
|
||||||
|
iced::widget::pick_list(
|
||||||
|
Some(selected_highlight_color),
|
||||||
|
color_choice_options.clone(),
|
||||||
|
|choice| choice.label.clone(),
|
||||||
|
)
|
||||||
|
.on_select(|choice| Message::SelectionHighlightColorChanged(choice.value))
|
||||||
|
.width(Fill),
|
||||||
|
]
|
||||||
|
.spacing(10)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
Space::new().height(24),
|
||||||
|
text(crate::t!("Grip Settings")).size(15),
|
||||||
|
Space::new().height(10),
|
||||||
|
row![
|
||||||
|
text(crate::t!("Grip size")).size(12).width(140),
|
||||||
|
slider(
|
||||||
|
1..=25,
|
||||||
|
model_space.grip_size.clamp(1, 25),
|
||||||
|
Message::GripSizeChanged,
|
||||||
|
)
|
||||||
|
.step(1)
|
||||||
|
.width(Fill),
|
||||||
|
text(format!("{} px", model_space.grip_size.clamp(1, 25)))
|
||||||
|
.size(11)
|
||||||
|
.width(44),
|
||||||
|
]
|
||||||
|
.spacing(10)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
Space::new().height(10),
|
||||||
|
row![
|
||||||
|
text(crate::t!("Unselected grip color")).size(12).width(140),
|
||||||
|
iced::widget::pick_list(
|
||||||
|
Some(selected_grip_color),
|
||||||
|
color_choice_options.clone(),
|
||||||
|
|choice| choice.label.clone(),
|
||||||
|
)
|
||||||
|
.on_select(|choice| Message::GripColorChanged(choice.value))
|
||||||
|
.width(Fill),
|
||||||
|
]
|
||||||
|
.spacing(10)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
Space::new().height(10),
|
||||||
|
row![
|
||||||
|
text(crate::t!("Selected/hot grip color")).size(12).width(140),
|
||||||
|
iced::widget::pick_list(
|
||||||
|
Some(selected_grip_hot),
|
||||||
|
color_choice_options.clone(),
|
||||||
|
|choice| choice.label.clone(),
|
||||||
|
)
|
||||||
|
.on_select(|choice| Message::GripHotChanged(choice.value))
|
||||||
|
.width(Fill),
|
||||||
|
]
|
||||||
|
.spacing(10)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
Space::new().height(10),
|
||||||
|
row![
|
||||||
|
text(crate::t!("Hover grip color")).size(12).width(140),
|
||||||
|
iced::widget::pick_list(
|
||||||
|
Some(selected_grip_hover),
|
||||||
|
color_choice_options,
|
||||||
|
|choice| choice.label.clone(),
|
||||||
|
)
|
||||||
|
.on_select(|choice| Message::GripHoverChanged(choice.value))
|
||||||
|
.width(Fill),
|
||||||
|
]
|
||||||
|
.spacing(10)
|
||||||
|
.align_y(iced::Center),
|
||||||
]
|
]
|
||||||
.spacing(0)
|
.spacing(0)
|
||||||
.width(sizing.width);
|
.width(sizing.width);
|
||||||
|
|
||||||
let content: Element<'a, Message> = match active_tab {
|
let content: Element<'a, Message> = match active_tab {
|
||||||
OptionsTab::General => general.into(),
|
OptionsTab::General => general.into(),
|
||||||
OptionsTab::Display => display.into(),
|
OptionsTab::Display => display_element.into(),
|
||||||
OptionsTab::Selection => selection.into(),
|
OptionsTab::Selection => selection.into(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user