Sketch Mirror, rectangular and circular Pattern, and Offset now show a green live preview that follows the selection and settings; OK commits, Cancel leaves the sketch unchanged. The shared dialogs show the current selection step and source counts, sources can be toggled with Ctrl/Cmd-click, cleared and reselected. Offsets keep editable driving distance dimensions (One Side, Two Sides, Symmetric) that update the derived curves without moving the source, with undo/redo and save/reopen. Format 19: older files keep their fixed offsets. New dialog labels in en, zh, ja, de and ko. Fixes #696. Fixes #517.
259 lines
9.7 KiB
Rust
259 lines
9.7 KiB
Rust
//! Colours and widget style: a dark theme (the default, a blue-grey in the spirit of desktop CAD
|
|
//! tools, viewport #2e3440) and a light one. Every colour the UI uses comes from [`Tokens`].
|
|
|
|
use std::sync::atomic::{AtomicBool, Ordering};
|
|
|
|
#[path = "../../../assets/styles/ui/palettes.rs"]
|
|
mod palettes;
|
|
|
|
use egui::{Color32, Stroke};
|
|
|
|
static DARK: AtomicBool = AtomicBool::new(true);
|
|
|
|
/// Is the dark theme on?
|
|
pub fn is_dark() -> bool {
|
|
DARK.load(Ordering::Relaxed)
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug)]
|
|
pub struct Tokens {
|
|
pub app_bar: Color32,
|
|
pub toolbar: Color32,
|
|
pub panel: Color32,
|
|
pub panel_header: Color32,
|
|
pub border: Color32,
|
|
pub text: Color32,
|
|
pub text_dim: Color32,
|
|
pub accent: Color32,
|
|
pub accent_soft: Color32,
|
|
pub sketch_accent: Color32,
|
|
pub error: Color32,
|
|
pub warning: Color32,
|
|
pub icon: Color32,
|
|
pub icon_fill: Color32,
|
|
pub timeline: Color32,
|
|
pub hover: Color32,
|
|
// ---- viewport selection feedback ----
|
|
/// Selected face (flat fill).
|
|
pub sel_face: Color32,
|
|
/// Selected edge core and its lighter rim.
|
|
pub sel_edge: Color32,
|
|
pub sel_edge_rim: Color32,
|
|
/// Hovered edge: dark core with a light halo.
|
|
pub hover_edge: Color32,
|
|
pub hover_edge_halo: Color32,
|
|
/// How much lighter a hovered face is (grey levels).
|
|
pub hover_face_lift: u8,
|
|
/// Selected profile fill and outline.
|
|
pub sel_profile: Color32,
|
|
pub sel_profile_edge: Color32,
|
|
/// Hovered profile: faint tint and a bright outline.
|
|
pub hover_profile: Color32,
|
|
pub hover_profile_edge: Color32,
|
|
/// Selected vertex dot.
|
|
pub sel_vertex: Color32,
|
|
// ---- origin ----
|
|
pub origin_plane: Color32,
|
|
pub origin_plane_edge: Color32,
|
|
pub origin_plane_hover: Color32,
|
|
pub origin_point: Color32,
|
|
pub axis_x: Color32,
|
|
pub axis_y: Color32,
|
|
pub axis_z: Color32,
|
|
/// Construction planes.
|
|
pub construction_plane: Color32,
|
|
/// Construction axes and points.
|
|
pub construction_geom: Color32,
|
|
// ---- bodies and live previews ----
|
|
pub body: Color32,
|
|
pub body_edge: Color32,
|
|
/// Command manipulators (direction arrows, handles).
|
|
pub manipulator: Color32,
|
|
/// New surface in a feature preview.
|
|
pub preview_add: Color32,
|
|
/// Proposed sketch geometry, distinct from blue selections.
|
|
pub preview_sketch: Color32,
|
|
/// Removed material in a feature preview (translucent).
|
|
pub preview_cut: Color32,
|
|
/// Navigation cube faces, outline and labels.
|
|
pub cube_face: Color32,
|
|
pub cube_border: Color32,
|
|
pub cube_text: Color32,
|
|
/// Light blue hover fill, readable on the pale cube in either theme.
|
|
pub cube_hover: Color32,
|
|
// ---- viewport ----
|
|
/// Viewport background (top and bottom of the gradient; equal for a flat fill).
|
|
pub viewport_top: Color32,
|
|
pub viewport_bottom: Color32,
|
|
pub grid: Color32,
|
|
pub grid_major: Color32,
|
|
/// A whole selected body.
|
|
pub body_selected: Color32,
|
|
/// Sketch curves: under-constrained, fully constrained (and finished sketches).
|
|
pub sketch: Color32,
|
|
pub sketch_fixed: Color32,
|
|
/// Fixed (locked) sketch geometry.
|
|
pub sketch_locked: Color32,
|
|
/// Sketch dimensions: driving, driven (reference).
|
|
pub dimension: Color32,
|
|
pub dimension_driven: Color32,
|
|
/// Sketch points: fully constrained, free.
|
|
pub sketch_point: Color32,
|
|
pub sketch_point_free: Color32,
|
|
/// Command dialogs: panel, border, and the filled selection chip.
|
|
pub dialog_bg: Color32,
|
|
pub dialog_border: Color32,
|
|
pub chip: Color32,
|
|
/// Band behind a panel's heading and, lighter, behind its section headings (both set off
|
|
/// from `dialog_bg`).
|
|
pub heading_bg: Color32,
|
|
pub subheading_bg: Color32,
|
|
/// Inside of bodies cut by a section.
|
|
pub section_cap: Color32,
|
|
/// Closed sketch profiles (translucent fill).
|
|
pub profile_fill: Color32,
|
|
/// Constraint glyph boxes.
|
|
pub glyph_bg: Color32,
|
|
pub glyph_edge: Color32,
|
|
pub glyph_text: Color32,
|
|
/// Chips drawn over the viewport (status, navigation bar, dimension labels).
|
|
pub overlay: Color32,
|
|
/// Rubber band of the shape being drawn.
|
|
pub rubber_band: Color32,
|
|
/// Light text on the application bar.
|
|
pub app_bar_text: Color32,
|
|
/// Timeline items: normal and failed.
|
|
pub timeline_item: Color32,
|
|
pub timeline_item_error: Color32,
|
|
pub timeline_marker: Color32,
|
|
/// Button and field backgrounds on panels.
|
|
pub field: Color32,
|
|
// ---- box selection ----
|
|
pub box_window: Color32,
|
|
pub box_crossing: Color32,
|
|
}
|
|
|
|
impl Tokens {
|
|
/// The tokens of the current theme.
|
|
pub fn get() -> Tokens {
|
|
if is_dark() { Tokens::dark() } else { Tokens::light() }
|
|
}
|
|
|
|
/// Fade idle cube colours into the viewport while retaining opaque facets.
|
|
pub fn navigation_cube(self, active: bool) -> Tokens {
|
|
if active {
|
|
return self;
|
|
}
|
|
let mut t = self;
|
|
t.cube_face = t.cube_face.lerp_to_gamma(t.viewport_top, 0.55);
|
|
t.cube_border = t.cube_border.lerp_to_gamma(t.viewport_top, 0.55);
|
|
t.cube_text = t.cube_text.lerp_to_gamma(t.viewport_top, 0.55);
|
|
t.axis_x = t.axis_x.lerp_to_gamma(t.viewport_top, 0.55);
|
|
t.axis_y = t.axis_y.lerp_to_gamma(t.viewport_top, 0.55);
|
|
t.axis_z = t.axis_z.lerp_to_gamma(t.viewport_top, 0.55);
|
|
t
|
|
}
|
|
}
|
|
|
|
/// How a physical material looks (the default appearance follows the material).
|
|
pub fn material_color(name: &str) -> Option<Color32> {
|
|
solidcraft_engine::doc::material_color(name).map(|[r, g, b]| Color32::from_rgb(r, g, b))
|
|
}
|
|
|
|
/// Switch themes (the visuals follow on the next [`apply`]).
|
|
pub fn set_dark(dark: bool) {
|
|
DARK.store(dark, Ordering::Relaxed);
|
|
}
|
|
|
|
fn visuals(dark: bool) -> egui::Visuals {
|
|
let t = if dark { Tokens::dark() } else { Tokens::light() };
|
|
let mut v = if dark { egui::Visuals::dark() } else { egui::Visuals::light() };
|
|
v.panel_fill = t.panel;
|
|
v.window_fill = t.panel;
|
|
v.window_stroke = Stroke::new(1.0, t.border);
|
|
v.extreme_bg_color = t.field;
|
|
v.faint_bg_color = t.panel_header;
|
|
v.override_text_color = Some(t.text);
|
|
v.widgets.inactive.weak_bg_fill = t.panel_header;
|
|
v.widgets.inactive.bg_fill = t.panel_header;
|
|
v.widgets.active.weak_bg_fill = t.accent_soft;
|
|
v.selection.bg_fill = t.accent_soft;
|
|
v.selection.stroke = Stroke::new(1.0, t.accent);
|
|
v.widgets.noninteractive.bg_stroke = Stroke::new(1.0, t.border);
|
|
v.widgets.noninteractive.fg_stroke = Stroke::new(1.0, t.text);
|
|
v.widgets.inactive.fg_stroke = Stroke::new(1.0, t.text);
|
|
v.widgets.hovered.weak_bg_fill = t.hover;
|
|
v.widgets.hovered.bg_fill = t.hover;
|
|
// egui uses the open-state fill for a movable window's active title bar.
|
|
v.widgets.open.weak_bg_fill = t.panel_header;
|
|
v.window_corner_radius = egui::CornerRadius::same(6);
|
|
v
|
|
}
|
|
|
|
pub fn apply(ctx: &egui::Context) {
|
|
let dark = is_dark();
|
|
ctx.set_theme(if dark { egui::Theme::Dark } else { egui::Theme::Light });
|
|
ctx.set_visuals(visuals(dark));
|
|
ctx.global_style_mut(|s| {
|
|
s.spacing.item_spacing = egui::vec2(6.0, 4.0);
|
|
s.spacing.button_padding = egui::vec2(6.0, 3.0);
|
|
});
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
/// Title and section bands stand out from a dialog, the title more than its sections.
|
|
#[test]
|
|
fn heading_bands_are_darker_than_the_dialog_in_order() {
|
|
let luma = |c: egui::Color32| u32::from(c.r()) * 3 + u32::from(c.g()) * 6 + u32::from(c.b());
|
|
for t in [super::Tokens::light(), super::Tokens::dark()] {
|
|
assert!(luma(t.heading_bg) < luma(t.subheading_bg), "{t:?}");
|
|
assert!(luma(t.subheading_bg) < luma(t.dialog_bg), "{t:?}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn active_movable_window_header_uses_theme_panel_header() {
|
|
for dark in [false, true] {
|
|
let ctx = egui::Context::default();
|
|
ctx.set_theme(if dark { egui::Theme::Dark } else { egui::Theme::Light });
|
|
ctx.set_visuals(super::visuals(dark));
|
|
let mut output = egui::FullOutput::default();
|
|
for _ in 0..3 {
|
|
let raw = egui::RawInput {
|
|
screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1400.0, 900.0))),
|
|
..Default::default()
|
|
};
|
|
output = ctx.run_ui(raw, |ui| {
|
|
crate::frame::window(ui.ctx(), crate::i18n::tr_label("Parameters"), crate::frame::Width::Normal).fade_in(false).show(
|
|
ui.ctx(),
|
|
|ui| {
|
|
ui.label(crate::i18n::tr_label("Parameter values"));
|
|
},
|
|
);
|
|
});
|
|
output.textures_delta.clear();
|
|
}
|
|
let title = output
|
|
.shapes
|
|
.iter()
|
|
.find_map(|shape| match &shape.shape {
|
|
egui::Shape::Text(text) if text.galley.text() == "Parameters" => Some(text.pos + text.galley.rect.center().to_vec2()),
|
|
_ => None,
|
|
})
|
|
.expect("window title must be drawn");
|
|
let header = output
|
|
.shapes
|
|
.iter()
|
|
.filter_map(|shape| match &shape.shape {
|
|
egui::Shape::Rect(rect) if rect.fill.a() > 0 && rect.rect.contains(title) => Some(rect),
|
|
_ => None,
|
|
})
|
|
.min_by(|a, b| a.rect.area().total_cmp(&b.rect.area()))
|
|
.expect("window header must be drawn behind its title");
|
|
let expected = if dark { super::Tokens::dark() } else { super::Tokens::light() };
|
|
assert_eq!(header.fill, expected.panel_header, "active title bar should follow the {dark} palette");
|
|
}
|
|
}
|
|
}
|