Dark theme (default): Fusion-like blue-grey viewport #2e3440, darker chrome, subtle grid, light text, blue accent; light theme kept as a second Tokens set; toggle on the application bar (sun/moon), ui.set {dark}, kept between runs (eframe storage; browser local storage on the web); viewport, sketch, glyph, view cube, timeline and overlay colours all come from the theme; preview spinner and state in ui.inspect
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
71a1903122
commit
956492c993
14 files changed
+290
-53
No files matched your search
Generated
+49
@@ -8,6 +8,8 @@ version = "0.24.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3b7f7f85a7e5f68090000ed7622545829afd484d210358702ae4cb97dd0c320"
|
||||
dependencies = [
|
||||
"enumn",
|
||||
"serde",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
@@ -130,6 +132,7 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"getrandom 0.3.4",
|
||||
"once_cell",
|
||||
"serde",
|
||||
"version_check",
|
||||
"zerocopy",
|
||||
]
|
||||
@@ -451,6 +454,9 @@ name = "bitflags"
|
||||
version = "2.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block2"
|
||||
@@ -863,6 +869,7 @@ checksum = "0656a7530b9e2841ee7a5f983965bdd9840c32f4012f759cfd44b912cced6b34"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"emath",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -891,6 +898,8 @@ dependencies = [
|
||||
"pollster 1.0.1",
|
||||
"profiling",
|
||||
"raw-window-handle",
|
||||
"ron",
|
||||
"serde",
|
||||
"static_assertions",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
@@ -915,6 +924,8 @@ dependencies = [
|
||||
"log",
|
||||
"nohash-hasher",
|
||||
"profiling",
|
||||
"ron",
|
||||
"serde",
|
||||
"smallvec",
|
||||
"unicode-segmentation",
|
||||
"web-sys",
|
||||
@@ -956,6 +967,7 @@ dependencies = [
|
||||
"objc2-ui-kit 0.3.2",
|
||||
"profiling",
|
||||
"raw-window-handle",
|
||||
"serde",
|
||||
"smithay-clipboard",
|
||||
"web-time",
|
||||
"winit",
|
||||
@@ -988,6 +1000,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52c8d4141cf0f60fb2f7aefaf352d3ffa5171ef0ab09731c64391801e95e48f1"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1017,6 +1030,17 @@ dependencies = [
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enumn"
|
||||
version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "epaint"
|
||||
version = "0.36.2"
|
||||
@@ -1035,6 +1059,7 @@ dependencies = [
|
||||
"parking_lot",
|
||||
"profiling",
|
||||
"self_cell",
|
||||
"serde",
|
||||
"skrifa",
|
||||
"smallvec",
|
||||
"unicode-general-category",
|
||||
@@ -1156,6 +1181,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c92d9081c7ab3bd83ef1eecdfad090f3d966e3ce74106779664114cfe1a6274"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2793,6 +2819,20 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e27ee8bb91ca0adcf0ecb116293afa12d393f9c2b9b9cd54d33e8078fe19839"
|
||||
|
||||
[[package]]
|
||||
name = "ron"
|
||||
version = "0.12.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81116b9531d61eabc41aeb228e4b6b2435bcca3233b98cf3b3077d4e6e9debb3"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"once_cell",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"typeid",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "1.1.0"
|
||||
@@ -3007,6 +3047,9 @@ name = "smallvec"
|
||||
version = "1.16.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smithay-client-toolkit"
|
||||
@@ -3582,6 +3625,12 @@ dependencies = [
|
||||
"rustc-hash 2.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typeid"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
|
||||
|
||||
[[package]]
|
||||
name = "uds_windows"
|
||||
version = "1.2.1"
|
||||
|
||||
+1
-1
@@ -46,7 +46,7 @@ image = { version = "0.25", default-features = false, features = ["png"] }
|
||||
|
||||
# ui
|
||||
egui = "0.36"
|
||||
eframe = { version = "0.36", default-features = false }
|
||||
eframe = { version = "0.36", default-features = false, features = ["persistence"] }
|
||||
# Instant/SystemTime that also work in the browser (std panics on wasm32-unknown-unknown).
|
||||
web-time = "1"
|
||||
egui-wgpu = "0.36"
|
||||
|
||||
@@ -28,8 +28,14 @@ mod web {
|
||||
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
|
||||
self.0.ui(ui);
|
||||
}
|
||||
fn save(&mut self, storage: &mut dyn eframe::Storage) {
|
||||
storage.set_string(PREFS_KEY, self.0.prefs());
|
||||
}
|
||||
}
|
||||
|
||||
/// Where the preferences (theme) are kept (the browser's local storage).
|
||||
const PREFS_KEY: &str = "solvecraft.prefs";
|
||||
|
||||
fn query() -> String {
|
||||
web_sys::window().and_then(|w| w.location().search().ok()).unwrap_or_default()
|
||||
}
|
||||
@@ -54,6 +60,9 @@ mod web {
|
||||
options,
|
||||
Box::new(move |cc| {
|
||||
let mut app = SolveApp::new(Session::default(), Services::default());
|
||||
if let Some(p) = cc.storage.and_then(|s| s.get_string(PREFS_KEY)) {
|
||||
app.load_prefs(&p);
|
||||
}
|
||||
if let Some(rs) = &cc.wgpu_render_state {
|
||||
app.set_wgpu(rs, DEPTH_BITS, 1);
|
||||
}
|
||||
|
||||
@@ -32,8 +32,14 @@ impl eframe::App for App {
|
||||
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
|
||||
self.0.ui(ui);
|
||||
}
|
||||
fn save(&mut self, storage: &mut dyn eframe::Storage) {
|
||||
storage.set_string(PREFS_KEY, self.0.prefs());
|
||||
}
|
||||
}
|
||||
|
||||
/// Where the preferences (theme) are kept in eframe's storage.
|
||||
const PREFS_KEY: &str = "solvecraft.prefs";
|
||||
|
||||
fn services() -> Services {
|
||||
Services {
|
||||
pick_open: Some(Box::new(|| {
|
||||
@@ -91,6 +97,9 @@ fn main() -> eframe::Result {
|
||||
options,
|
||||
Box::new(move |cc| {
|
||||
let mut app = SolveApp::new(Session::default(), services());
|
||||
if let Some(p) = cc.storage.and_then(|s| s.get_string(PREFS_KEY)) {
|
||||
app.load_prefs(&p);
|
||||
}
|
||||
if let Some(rs) = &cc.wgpu_render_state {
|
||||
app.set_wgpu(rs, DEPTH_BITS, u32::from(MSAA));
|
||||
}
|
||||
|
||||
@@ -95,6 +95,7 @@ pub fn handle(app: &mut SolveApp, ctx: &egui::Context, req: &ControlRequest) ->
|
||||
"renderer": if app.viewport.gpu.is_some() { "gpu" } else { "cpu" },
|
||||
"frame_ms": app.frame_ms,
|
||||
"build_ms": app.viewport.build_ms,
|
||||
"preview": {"active": app.preview.active, "busy": app.preview.busy, "error": app.preview.error, "ms": app.preview.ms, "replaced": app.preview.replaced},
|
||||
})),
|
||||
"ui.set" => {
|
||||
let mut cur = serde_json::to_value(&app.ui).unwrap_or(json!({}));
|
||||
|
||||
@@ -702,6 +702,9 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
if ui.add(egui::Button::new(close).min_size(vec2(70.0, 24.0))).clicked() {
|
||||
cancel = true;
|
||||
}
|
||||
if app.preview.busy {
|
||||
ui.add(egui::Spinner::new().size(14.0)).on_hover_text("Updating the preview");
|
||||
}
|
||||
});
|
||||
});
|
||||
// Keyboard: Enter applies (from a value field, or with nothing focused), Esc cancels.
|
||||
|
||||
@@ -183,6 +183,22 @@ pub fn paint(p: &Painter, r: Rect, name: &str, ink: Color32, fill: Color32, acce
|
||||
pen.poly(&[(3.0, 7.0), (9.0, 10.0), (9.0, 17.0), (3.0, 17.0)], pen.fill, ink);
|
||||
pen.poly(&[(21.0, 7.0), (15.0, 10.0), (15.0, 17.0), (21.0, 17.0)], Color32::TRANSPARENT, a);
|
||||
}
|
||||
"sun" => {
|
||||
pen.circle(12.0, 12.0, 4.5, a, ink);
|
||||
for i in 0..8 {
|
||||
let t = i as f32 / 8.0 * std::f32::consts::TAU;
|
||||
pen.line(&[(12.0 + 7.0 * t.cos(), 12.0 + 7.0 * t.sin()), (12.0 + 9.5 * t.cos(), 12.0 + 9.5 * t.sin())], ink);
|
||||
}
|
||||
}
|
||||
"moon" => {
|
||||
let outer: Vec<Pos2> =
|
||||
(0..=24).map(|i| -0.6 + i as f32 / 24.0 * 4.4).map(|t| pen.at(12.0 + 8.0 * t.cos(), 12.0 + 8.0 * t.sin())).collect();
|
||||
let inner: Vec<Pos2> =
|
||||
(0..=24).rev().map(|i| -0.9 + i as f32 / 24.0 * 3.9).map(|t| pen.at(15.5 + 6.5 * t.cos(), 9.5 + 6.5 * t.sin())).collect();
|
||||
let mut pts = outer;
|
||||
pts.extend(inner);
|
||||
p.add(Shape::closed_line(pts, Stroke::new(pen.w, ink)));
|
||||
}
|
||||
"presspull" => {
|
||||
pen.poly(&[(3.0, 15.0), (12.0, 11.0), (21.0, 15.0), (12.0, 19.0)], pen.fill, ink);
|
||||
pen.arrow((12.0, 15.0), (12.0, 3.0), a);
|
||||
|
||||
@@ -51,6 +51,8 @@ pub struct UiState {
|
||||
pub hidden_origin: Vec<String>,
|
||||
pub show_sketches: bool,
|
||||
pub palette_open: bool,
|
||||
/// Dark theme (the default); a preference kept between runs.
|
||||
pub dark: bool,
|
||||
}
|
||||
|
||||
impl Default for UiState {
|
||||
@@ -66,6 +68,7 @@ impl Default for UiState {
|
||||
hidden_origin: Vec::new(),
|
||||
show_sketches: true,
|
||||
palette_open: false,
|
||||
dark: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -143,6 +146,20 @@ impl SolveApp {
|
||||
self.viewport.gpu = Some(gpu::install(rs, depth_bits, samples));
|
||||
}
|
||||
|
||||
/// Preferences kept between runs (JSON), for the host to store.
|
||||
pub fn prefs(&self) -> String {
|
||||
json!({ "dark": self.ui.dark }).to_string()
|
||||
}
|
||||
|
||||
/// Restore preferences saved with [`SolveApp::prefs`].
|
||||
pub fn load_prefs(&mut self, prefs: &str) {
|
||||
if let Ok(v) = serde_json::from_str::<Value>(prefs)
|
||||
&& let Some(d) = v.get("dark").and_then(Value::as_bool)
|
||||
{
|
||||
self.ui.dark = d;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_control(mut self, rx: Receiver<ControlRequest>) -> Self {
|
||||
self.control_rx = Some(rx);
|
||||
self
|
||||
@@ -402,7 +419,8 @@ impl SolveApp {
|
||||
|
||||
/// Per-frame logic before layout.
|
||||
pub fn logic(&mut self, ctx: &egui::Context) {
|
||||
if !self.styled {
|
||||
if !self.styled || theme::is_dark() != self.ui.dark {
|
||||
theme::set_dark(self.ui.dark);
|
||||
theme::apply(ctx);
|
||||
self.styled = true;
|
||||
}
|
||||
|
||||
+134
-5
@@ -1,8 +1,17 @@
|
||||
//! Colours and widget style. A light theme in the spirit of desktop CAD tools; every colour
|
||||
//! the UI uses comes from [`Tokens`].
|
||||
//! 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};
|
||||
|
||||
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,
|
||||
@@ -60,13 +69,51 @@ pub struct Tokens {
|
||||
pub preview_add: Color32,
|
||||
/// Removed material in a feature preview (translucent).
|
||||
pub preview_cut: 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,
|
||||
/// Sketch points: fully constrained, free.
|
||||
pub sketch_point: Color32,
|
||||
pub sketch_point_free: 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,
|
||||
/// View cube face (shaded by orientation) and edges.
|
||||
pub cube_face: Color32,
|
||||
pub cube_edge: 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 {
|
||||
pub const fn get() -> Tokens {
|
||||
/// The tokens of the current theme.
|
||||
pub fn get() -> Tokens {
|
||||
if is_dark() { Tokens::dark() } else { Tokens::light() }
|
||||
}
|
||||
|
||||
pub const fn light() -> Tokens {
|
||||
Tokens {
|
||||
app_bar: Color32::from_rgb(52, 56, 64),
|
||||
toolbar: Color32::from_rgb(246, 247, 249),
|
||||
@@ -109,18 +156,99 @@ impl Tokens {
|
||||
manipulator: Color32::from_rgb(40, 110, 220),
|
||||
preview_add: Color32::from_rgb(112, 160, 226),
|
||||
preview_cut: Color32::from_rgba_unmultiplied_const(232, 84, 44, 110),
|
||||
viewport_top: Color32::from_rgb(240, 243, 247),
|
||||
viewport_bottom: Color32::from_rgb(203, 210, 221),
|
||||
grid: Color32::from_rgb(196, 202, 212),
|
||||
grid_major: Color32::from_rgb(170, 178, 190),
|
||||
body_selected: Color32::from_rgb(110, 170, 235),
|
||||
sketch: Color32::from_rgb(30, 90, 200),
|
||||
sketch_fixed: Color32::from_rgb(20, 20, 20),
|
||||
sketch_point: Color32::BLACK,
|
||||
sketch_point_free: Color32::from_rgb(30, 90, 200),
|
||||
glyph_bg: Color32::WHITE,
|
||||
glyph_edge: Color32::from_rgb(90, 150, 90),
|
||||
glyph_text: Color32::from_rgb(40, 110, 40),
|
||||
overlay: Color32::from_rgba_unmultiplied_const(255, 255, 255, 235),
|
||||
cube_face: Color32::from_rgb(236, 236, 236),
|
||||
cube_edge: Color32::from_rgb(140, 148, 160),
|
||||
rubber_band: Color32::from_rgb(30, 120, 230),
|
||||
app_bar_text: Color32::from_rgb(230, 234, 242),
|
||||
timeline_item: Color32::WHITE,
|
||||
timeline_item_error: Color32::from_rgb(250, 220, 220),
|
||||
timeline_marker: Color32::from_rgb(70, 76, 88),
|
||||
field: Color32::WHITE,
|
||||
box_window: Color32::from_rgb(38, 120, 218),
|
||||
box_crossing: Color32::from_rgb(40, 160, 80),
|
||||
}
|
||||
}
|
||||
|
||||
/// Dark blue-grey: viewport #2e3440 as measured in the reference tool, chrome a little
|
||||
/// darker, light text, a blue accent.
|
||||
pub const fn dark() -> Tokens {
|
||||
let mut t = Tokens::light();
|
||||
t.app_bar = Color32::from_rgb(28, 31, 38);
|
||||
t.toolbar = Color32::from_rgb(37, 41, 49);
|
||||
t.tab_active = Color32::from_rgb(46, 52, 64);
|
||||
t.panel = Color32::from_rgb(39, 43, 52);
|
||||
t.panel_header = Color32::from_rgb(47, 52, 62);
|
||||
t.border = Color32::from_rgb(63, 69, 82);
|
||||
t.text = Color32::from_rgb(222, 226, 232);
|
||||
t.text_dim = Color32::from_rgb(148, 155, 168);
|
||||
t.accent = Color32::from_rgb(58, 136, 238);
|
||||
t.accent_soft = Color32::from_rgb(42, 70, 110);
|
||||
t.sketch_accent = Color32::from_rgb(110, 200, 140);
|
||||
t.error = Color32::from_rgb(240, 100, 96);
|
||||
t.warning = Color32::from_rgb(236, 170, 70);
|
||||
t.icon = Color32::from_rgb(208, 214, 226);
|
||||
t.icon_fill = Color32::from_rgb(86, 112, 150);
|
||||
t.timeline = Color32::from_rgb(33, 37, 45);
|
||||
t.hover = Color32::from_rgb(54, 61, 74);
|
||||
t.hover_edge_halo = Color32::from_rgb(203, 203, 203);
|
||||
t.origin_point = Color32::from_rgb(214, 215, 218);
|
||||
t.viewport_top = Color32::from_rgb(46, 52, 64);
|
||||
t.viewport_bottom = Color32::from_rgb(46, 52, 64);
|
||||
t.grid = Color32::from_rgb(59, 65, 76);
|
||||
t.grid_major = Color32::from_rgb(73, 79, 90);
|
||||
t.body = Color32::from_rgb(152, 156, 162);
|
||||
t.body_edge = Color32::from_rgb(16, 16, 18);
|
||||
t.sketch = Color32::from_rgb(92, 150, 255);
|
||||
t.sketch_fixed = Color32::from_rgb(232, 234, 238);
|
||||
t.sketch_point = Color32::from_rgb(232, 234, 238);
|
||||
t.sketch_point_free = Color32::from_rgb(92, 150, 255);
|
||||
t.glyph_bg = Color32::from_rgb(44, 50, 60);
|
||||
t.glyph_edge = Color32::from_rgb(96, 170, 110);
|
||||
t.glyph_text = Color32::from_rgb(150, 220, 160);
|
||||
t.overlay = Color32::from_rgba_unmultiplied_const(34, 38, 46, 240);
|
||||
t.cube_face = Color32::from_rgb(196, 200, 207);
|
||||
t.cube_edge = Color32::from_rgb(100, 108, 122);
|
||||
t.rubber_band = Color32::from_rgb(92, 160, 255);
|
||||
t.timeline_item = Color32::from_rgb(52, 58, 70);
|
||||
t.timeline_item_error = Color32::from_rgb(110, 50, 54);
|
||||
t.timeline_marker = Color32::from_rgb(170, 178, 192);
|
||||
t.field = Color32::from_rgb(30, 34, 41);
|
||||
t.box_window = Color32::from_rgb(70, 150, 255);
|
||||
t.box_crossing = Color32::from_rgb(80, 200, 120);
|
||||
t
|
||||
}
|
||||
}
|
||||
|
||||
/// Switch themes (the visuals follow on the next [`apply`]).
|
||||
pub fn set_dark(dark: bool) {
|
||||
DARK.store(dark, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub fn apply(ctx: &egui::Context) {
|
||||
let t = Tokens::get();
|
||||
let mut v = egui::Visuals::light();
|
||||
let mut v = if is_dark() { egui::Visuals::dark() } else { egui::Visuals::light() };
|
||||
v.panel_fill = t.panel;
|
||||
v.window_fill = t.panel;
|
||||
v.extreme_bg_color = Color32::WHITE;
|
||||
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);
|
||||
@@ -129,6 +257,7 @@ pub fn apply(ctx: &egui::Context) {
|
||||
v.widgets.hovered.weak_bg_fill = t.hover;
|
||||
v.widgets.hovered.bg_fill = t.hover;
|
||||
v.window_corner_radius = egui::CornerRadius::same(6);
|
||||
ctx.set_theme(if is_dark() { egui::Theme::Dark } else { egui::Theme::Light });
|
||||
ctx.set_visuals(v);
|
||||
ctx.global_style_mut(|s| {
|
||||
s.spacing.item_spacing = egui::vec2(6.0, 4.0);
|
||||
|
||||
@@ -142,13 +142,13 @@ pub fn timeline(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
hovered = Some(*id);
|
||||
}
|
||||
let bg = if err.is_some() {
|
||||
Color32::from_rgb(250, 220, 220)
|
||||
t.timeline_item_error
|
||||
} else if sel {
|
||||
t.accent_soft
|
||||
} else if resp.hovered() || from_view == Some(*id) {
|
||||
t.hover
|
||||
} else {
|
||||
Color32::WHITE
|
||||
t.timeline_item
|
||||
};
|
||||
let edge = if err.is_some() {
|
||||
t.error
|
||||
@@ -236,7 +236,7 @@ pub fn timeline(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
if resp.drag_started() {
|
||||
drag = Some(Drag::Marker);
|
||||
}
|
||||
let col = if resp.hovered() || drag == Some(Drag::Marker) { t.accent } else { Color32::from_rgb(70, 76, 88) };
|
||||
let col = if resp.hovered() || drag == Some(Drag::Marker) { t.accent } else { t.timeline_marker };
|
||||
p.rect_filled(mr, 1.0, col);
|
||||
resp.on_hover_text("History marker: drag to roll the model back; new features go here");
|
||||
}
|
||||
|
||||
@@ -41,13 +41,7 @@ pub fn app_bar(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
// Brand mark: a small solid block drawn in code.
|
||||
let logo = Rect::from_center_size(pos2(r.left() + 20.0, r.center().y), vec2(20.0, 20.0));
|
||||
icons::paint(ui.painter(), logo, "box", Color32::from_rgb(220, 226, 236), Color32::from_rgb(90, 160, 240), Color32::WHITE);
|
||||
ui.painter().text(
|
||||
pos2(r.left() + 36.0, r.center().y),
|
||||
Align2::LEFT_CENTER,
|
||||
"SolveCraft",
|
||||
FontId::proportional(14.0),
|
||||
Color32::from_rgb(230, 234, 242),
|
||||
);
|
||||
ui.painter().text(pos2(r.left() + 36.0, r.center().y), Align2::LEFT_CENTER, "SolveCraft", FontId::proportional(14.0), t.app_bar_text);
|
||||
let mut x = r.left() + 128.0;
|
||||
let mut click = |ui: &mut egui::Ui, icon: &str, tip: &str| -> bool {
|
||||
let br = Rect::from_center_size(pos2(x, r.center().y), vec2(26.0, 26.0));
|
||||
@@ -76,6 +70,10 @@ pub fn app_bar(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
if click(ui, "redo", "Redo (Ctrl+Y)") {
|
||||
let _ = app.run("RedoCommand", json!({}));
|
||||
}
|
||||
let theme_tip = if app.ui.dark { "Light theme" } else { "Dark theme" };
|
||||
if click(ui, if app.ui.dark { "sun" } else { "moon" }, theme_tip) {
|
||||
app.ui.dark = !app.ui.dark;
|
||||
}
|
||||
if file {
|
||||
app.ui.palette_open = false;
|
||||
let ctx = ui.ctx().clone();
|
||||
@@ -206,7 +204,7 @@ pub fn toolbar(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
// Workspace selector and tabs.
|
||||
let tabs_y = r.top() + 2.0;
|
||||
let ws = Rect::from_min_size(pos2(r.left() + 8.0, tabs_y + 26.0), vec2(96.0, 56.0));
|
||||
painter.rect(ws, 4.0, Color32::WHITE, Stroke::new(1.0, t.border), egui::StrokeKind::Inside);
|
||||
painter.rect(ws, 4.0, t.field, Stroke::new(1.0, t.border), egui::StrokeKind::Inside);
|
||||
painter.text(pos2(ws.center().x - 5.0, ws.top() + 14.0), Align2::CENTER_CENTER, "DESIGN", FontId::proportional(12.0), t.text);
|
||||
caret(&painter, pos2(ws.center().x + 26.0, ws.top() + 14.0), t.text);
|
||||
painter.text(pos2(ws.center().x, ws.top() + 34.0), Align2::CENTER_CENTER, "workspace", FontId::proportional(10.0), t.text_dim);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Interactive sketch tools: click points on the sketch plane (or pick sketch entities) and the
|
||||
//! tool runs the matching command. Tools are UI state only; the work is done by commands.
|
||||
|
||||
use egui::{Color32, Pos2, Shape, Stroke};
|
||||
use egui::{Pos2, Shape, Stroke};
|
||||
use serde_json::{Value, json};
|
||||
use solvecraft_engine::geom::Vec2;
|
||||
|
||||
@@ -265,7 +265,8 @@ pub fn preview(app: &SolveApp, t: &Tool, painter: &egui::Painter, proj: &Proj) {
|
||||
let st = app.session.model.state();
|
||||
let Some(ss) = st.sketch(sid) else { return };
|
||||
let to = |p: Vec2| proj.to_screen(ss.plane.to_world(p));
|
||||
let stroke = Stroke::new(1.5, Color32::from_rgb(30, 120, 230));
|
||||
let tk = crate::theme::Tokens::get();
|
||||
let stroke = Stroke::new(1.5, tk.rubber_band);
|
||||
if let Some((hp, snap)) = &t.hover
|
||||
&& let Some(sp) = to(*hp)
|
||||
{
|
||||
@@ -275,7 +276,7 @@ pub fn preview(app: &SolveApp, t: &Tool, painter: &egui::Painter, proj: &Proj) {
|
||||
egui::Align2::LEFT_TOP,
|
||||
format!("{:.2}, {:.2}", hp.x, hp.y),
|
||||
egui::FontId::proportional(11.0),
|
||||
Color32::from_rgb(60, 70, 90),
|
||||
tk.text_dim,
|
||||
);
|
||||
}
|
||||
let Some((h, _)) = t.hover.clone() else { return };
|
||||
|
||||
@@ -144,6 +144,7 @@ fn scene_key(app: &SolveApp) -> u64 {
|
||||
app.ui.show_sketches.hash(&mut h);
|
||||
app.ui.hidden_bodies.hash(&mut h);
|
||||
app.preview.replaced.hash(&mut h);
|
||||
app.ui.dark.hash(&mut h);
|
||||
app.session.active_sketch.hash(&mut h);
|
||||
let (minor, _) = grid_step(app.cam.half_height());
|
||||
minor.to_bits().hash(&mut h);
|
||||
@@ -152,7 +153,19 @@ fn scene_key(app: &SolveApp) -> u64 {
|
||||
h.finish()
|
||||
}
|
||||
|
||||
/// A sketch curve colour from the engine, in the current theme.
|
||||
fn sketch_color(t: &Tokens, c: Rgb) -> [u8; 4] {
|
||||
if c == colors::SKETCH {
|
||||
c4(t.sketch)
|
||||
} else if c == colors::SKETCH_DONE || c == colors::SKETCH_FIXED {
|
||||
c4(t.sketch_fixed)
|
||||
} else {
|
||||
rgba(c)
|
||||
}
|
||||
}
|
||||
|
||||
fn build_scene(app: &SolveApp) -> GpuScene {
|
||||
let tk = Tokens::get();
|
||||
let mut sc = GpuScene::default();
|
||||
let s = &app.session;
|
||||
let st = s.model.state();
|
||||
@@ -164,7 +177,7 @@ fn build_scene(app: &SolveApp) -> GpuScene {
|
||||
for i in -n..=n {
|
||||
let t = i as f64 * minor;
|
||||
let maj = (t / major).round() * major == t;
|
||||
let col = if maj { rgba(colors::GRID_MAJOR) } else { rgba(colors::GRID) };
|
||||
let col = if maj { c4(tk.grid_major) } else { c4(tk.grid) };
|
||||
let w = if maj { 1.0 } else { 0.7 };
|
||||
sc.line((c + Vec3::new(t, -ext, 0.0)).to_f32(), (c + Vec3::new(t, ext, 0.0)).to_f32(), col, w, false);
|
||||
sc.line((c + Vec3::new(-ext, t, 0.0)).to_f32(), (c + Vec3::new(ext, t, 0.0)).to_f32(), col, w, false);
|
||||
@@ -174,7 +187,7 @@ fn build_scene(app: &SolveApp) -> GpuScene {
|
||||
if app.ui.hidden_bodies.contains(&b.name) || app.preview.replaced.contains(&b.name) {
|
||||
continue;
|
||||
}
|
||||
let col = rgba(colors::BODY);
|
||||
let col = c4(tk.body);
|
||||
let m = b.mesh();
|
||||
for t in &m.triangles {
|
||||
for k in t {
|
||||
@@ -189,7 +202,7 @@ fn build_scene(app: &SolveApp) -> GpuScene {
|
||||
continue;
|
||||
}
|
||||
for w in e.windows(2) {
|
||||
sc.line(w[0].to_f32(), w[1].to_f32(), rgba(colors::EDGE), 1.3, false);
|
||||
sc.line(w[0].to_f32(), w[1].to_f32(), c4(tk.body_edge), 1.3, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -198,7 +211,7 @@ fn build_scene(app: &SolveApp) -> GpuScene {
|
||||
let active = s.active_sketch == Some(sid);
|
||||
for (pts, col, cons) in sketch_lines(&ss.sketch, &ss.plane, active, &ss.report.curve_determined) {
|
||||
for w in pts.windows(2) {
|
||||
sc.line(w[0].to_f32(), w[1].to_f32(), rgba(col), if cons { 1.2 } else { 2.0 }, active);
|
||||
sc.line(w[0].to_f32(), w[1].to_f32(), sketch_color(&tk, col), if cons { 1.2 } else { 2.0 }, active);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -505,6 +518,7 @@ fn highlight_key(app: &SolveApp) -> u64 {
|
||||
serde_json::to_string(&app.highlighted()).unwrap_or_default().hash(&mut h);
|
||||
format!("{:?}", app.viewport.hover).hash(&mut h);
|
||||
app.viewport.hover_feature.hash(&mut h);
|
||||
app.ui.dark.hash(&mut h);
|
||||
app.preview.replaced.hash(&mut h);
|
||||
app.origin_visible().hash(&mut h);
|
||||
app.ui.hidden_origin.hash(&mut h);
|
||||
@@ -573,13 +587,7 @@ fn build_highlight(app: &SolveApp) -> GpuScene {
|
||||
{
|
||||
let m = b.mesh();
|
||||
for f in 0..b.body.face_count() {
|
||||
face_tris(
|
||||
&mut sc,
|
||||
&m,
|
||||
f,
|
||||
c4(Color32::from_rgb(colors::BODY_SELECTED.0, colors::BODY_SELECTED.1, colors::BODY_SELECTED.2)),
|
||||
true,
|
||||
);
|
||||
face_tris(&mut sc, &m, f, c4(t.body_selected), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -609,7 +617,7 @@ fn build_highlight(app: &SolveApp) -> GpuScene {
|
||||
// Bodies made by the timeline item under the cursor.
|
||||
if let Some(fid) = app.viewport.hover_feature {
|
||||
let k = t.hover_face_lift;
|
||||
let c = colors::BODY;
|
||||
let c = (t.body.r(), t.body.g(), t.body.b());
|
||||
for b in st.bodies.iter().filter(|b| b.feature == fid && !app.ui.hidden_bodies.contains(&b.name)) {
|
||||
let m = b.mesh();
|
||||
for f in 0..b.body.face_count() {
|
||||
@@ -630,7 +638,7 @@ fn build_highlight(app: &SolveApp) -> GpuScene {
|
||||
{
|
||||
if let Some(b) = st.body(body) {
|
||||
let k = t.hover_face_lift;
|
||||
let c = colors::BODY;
|
||||
let c = (t.body.r(), t.body.g(), t.body.b());
|
||||
face_tris(&mut sc, &b.mesh(), *index, [c.0.saturating_add(k), c.1.saturating_add(k), c.2.saturating_add(k), 255], true);
|
||||
let m = b.mesh();
|
||||
for e in m.face_edges(u32::try_from(*index).unwrap_or(u32::MAX)) {
|
||||
@@ -790,8 +798,7 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
let proj = projection(app, rect);
|
||||
let painter = ui.painter_at(rect);
|
||||
// Background gradient.
|
||||
let top = Color32::from_rgb(colors::BG_TOP.0, colors::BG_TOP.1, colors::BG_TOP.2);
|
||||
let bot = Color32::from_rgb(colors::BG_BOTTOM.0, colors::BG_BOTTOM.1, colors::BG_BOTTOM.2);
|
||||
let (top, bot) = (t.viewport_top, t.viewport_bottom);
|
||||
let mut mesh = egui::Mesh::default();
|
||||
mesh.colored_vertex(rect.left_top(), top);
|
||||
mesh.colored_vertex(rect.right_top(), top);
|
||||
@@ -911,7 +918,7 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
let pos = pos2(rect.left() + 12.0, rect.bottom() - 46.0);
|
||||
let galley = painter.layout_no_wrap(msg, FontId::proportional(12.5), if err { t.error } else { t.text });
|
||||
let r = Rect::from_min_size(pos, galley.size() + vec2(16.0, 8.0));
|
||||
painter.rect_filled(r, 4.0, Color32::from_white_alpha(230));
|
||||
painter.rect_filled(r, 4.0, t.overlay);
|
||||
painter.galley(pos + vec2(8.0, 4.0), galley, t.text);
|
||||
ui.ctx().request_repaint_after(std::time::Duration::from_millis(500));
|
||||
} else {
|
||||
@@ -1081,9 +1088,9 @@ fn overlays(app: &mut SolveApp, ui: &mut egui::Ui, painter: &egui::Painter, proj
|
||||
for (i, p) in sk.points.iter().enumerate() {
|
||||
let Some(sp) = proj.to_screen(ss.plane.to_world(p.pos)) else { continue };
|
||||
let det = ss.report.point_determined.get(i).copied().unwrap_or(false);
|
||||
let c = if det { Color32::BLACK } else { Color32::from_rgb(30, 90, 200) };
|
||||
let c = if det { t.sketch_point } else { t.sketch_point_free };
|
||||
if i == 0 {
|
||||
painter.circle(sp, 3.5, Color32::from_rgb(240, 200, 60), Stroke::new(1.0, Color32::BLACK));
|
||||
painter.circle(sp, 3.5, Color32::from_rgb(240, 200, 60), Stroke::new(1.0, t.body_edge));
|
||||
} else {
|
||||
painter.rect_filled(Rect::from_center_size(sp, vec2(5.0, 5.0)), 0.0, c);
|
||||
}
|
||||
@@ -1104,7 +1111,7 @@ fn overlays(app: &mut SolveApp, ui: &mut egui::Ui, painter: &egui::Painter, proj
|
||||
};
|
||||
let galley = painter.layout_no_wrap(text, FontId::proportional(12.0), t.text);
|
||||
let r = Rect::from_center_size(a, galley.size() + vec2(8.0, 4.0));
|
||||
painter.rect(r, 2.0, Color32::from_white_alpha(235), Stroke::new(1.0, t.border), egui::StrokeKind::Inside);
|
||||
painter.rect(r, 2.0, t.overlay, Stroke::new(1.0, t.border), egui::StrokeKind::Inside);
|
||||
painter.galley(r.min + vec2(4.0, 2.0), galley, t.text);
|
||||
let resp = ui.interact(r, ui.id().with(("dim", param.as_str())), Sense::click());
|
||||
if resp.double_clicked() || resp.clicked() && app.tool.is_none() {
|
||||
@@ -1174,9 +1181,10 @@ fn glyph(painter: &egui::Painter, proj: &Proj, ss: &solvecraft_engine::doc::Solv
|
||||
};
|
||||
let Some(a) = anchor.and_then(|a| proj.to_screen(ss.plane.to_world(a))) else { return };
|
||||
let r = Rect::from_center_size(a + vec2(10.0, -10.0), vec2(13.0, 13.0));
|
||||
painter.rect_filled(r, 2.0, Color32::from_rgb(255, 255, 255));
|
||||
painter.rect_stroke(r, 2.0, Stroke::new(1.0, Color32::from_rgb(90, 150, 90)), egui::StrokeKind::Inside);
|
||||
painter.text(r.center(), Align2::CENTER_CENTER, label, FontId::proportional(10.0), Color32::from_rgb(40, 110, 40));
|
||||
let t = Tokens::get();
|
||||
painter.rect_filled(r, 2.0, t.glyph_bg);
|
||||
painter.rect_stroke(r, 2.0, Stroke::new(1.0, t.glyph_edge), egui::StrokeKind::Inside);
|
||||
painter.text(r.center(), Align2::CENTER_CENTER, label, FontId::proportional(10.0), t.glyph_text);
|
||||
}
|
||||
|
||||
fn cpu_render(app: &mut SolveApp, ctx: &egui::Context, painter: &egui::Painter, rect: Rect, proj: &Proj) {
|
||||
@@ -1238,11 +1246,7 @@ fn view_cube(app: &mut SolveApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
let pts: Vec<Pos2> = corners.iter().map(|p| to2(*p).0).collect();
|
||||
let poly_hover = hover.is_some_and(|h| point_in_poly(h, &pts));
|
||||
let shade = (0.75 + 0.25 * n.dot(b)) as f32;
|
||||
painter.add(Shape::convex_polygon(
|
||||
pts.clone(),
|
||||
Color32::from_gray((236.0 * shade) as u8),
|
||||
Stroke::new(1.0, Color32::from_rgb(140, 148, 160)),
|
||||
));
|
||||
painter.add(Shape::convex_polygon(pts.clone(), t.cube_face.gamma_multiply(shade).to_opaque(), Stroke::new(1.0, t.cube_edge)));
|
||||
// Hover zone: the outer band of a face picks the edge or corner it borders.
|
||||
let zone = hover.filter(|_| poly_hover).and_then(|h| cube_zone(&pts, h));
|
||||
if let Some((zi, zj)) = zone {
|
||||
@@ -1257,7 +1261,7 @@ fn view_cube(app: &mut SolveApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
}
|
||||
let center = to2(n).0;
|
||||
if n.dot(b) > 0.35 {
|
||||
painter.text(center, Align2::CENTER_CENTER, label, FontId::proportional(9.5), t.text);
|
||||
painter.text(center, Align2::CENTER_CENTER, label, FontId::proportional(9.5), Color32::from_rgb(40, 44, 52));
|
||||
}
|
||||
if let Some((zi, zj)) = zone
|
||||
&& ui.input(|i| i.pointer.primary_clicked())
|
||||
@@ -1274,7 +1278,7 @@ fn view_cube(app: &mut SolveApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
}
|
||||
let home = Rect::from_center_size(pos2(c.x - 52.0, c.y - 48.0), vec2(18.0, 18.0));
|
||||
let hr = ui.interact(home, ui.id().with("vc_home"), Sense::click());
|
||||
icons::paint(&painter, home, "home", t.icon, if hr.hovered() { t.accent_soft } else { Color32::WHITE }, t.accent);
|
||||
icons::paint(&painter, home, "home", t.icon, if hr.hovered() { t.accent_soft } else { t.overlay }, t.accent);
|
||||
if hr.on_hover_text("Home view").clicked() {
|
||||
app.animate_view("home");
|
||||
}
|
||||
@@ -1345,7 +1349,7 @@ fn nav_bar(app: &mut SolveApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
let w = items.len() as f32 * 30.0 + 10.0;
|
||||
let bar = Rect::from_center_size(pos2(rect.center().x, rect.bottom() - 22.0), vec2(w, 30.0));
|
||||
let painter = ui.painter_at(rect);
|
||||
painter.rect(bar, 6.0, Color32::from_white_alpha(235), Stroke::new(1.0, t.border), egui::StrokeKind::Inside);
|
||||
painter.rect(bar, 6.0, t.overlay, Stroke::new(1.0, t.border), egui::StrokeKind::Inside);
|
||||
for (i, (icon, tip)) in items.iter().enumerate() {
|
||||
let br = Rect::from_min_size(pos2(bar.left() + 5.0 + i as f32 * 30.0, bar.top() + 2.0), vec2(26.0, 26.0));
|
||||
let resp = ui.interact(br, ui.id().with(("nav", *icon)), Sense::click());
|
||||
|
||||
@@ -21,8 +21,8 @@ Commands never open dialogs when run this way. `solvecraft-cli commands` prints
|
||||
|
||||
| Method | Params | Notes |
|
||||
|---|---|---|
|
||||
| `ui.inspect` | — | UI state, viewport rect, camera, active tool/dialog, hover, renderer, timings |
|
||||
| `ui.set` | any `UiState` field (`tab`, `perspective`, `showGrid`, `hiddenBodies`, …) | |
|
||||
| `ui.inspect` | — | UI state, viewport rect, camera, active tool/dialog, hover, renderer, timings, live preview (`active`, `busy`, `error`, `ms`, bodies it stands in for) |
|
||||
| `ui.set` | any `UiState` field (`tab`, `perspective`, `showGrid`, `hiddenBodies`, `dark`, …) | `dark: false` switches to the light theme |
|
||||
| `ui.view` | `view`: `front`, `back`, `top`, `bottom`, `left`, `right`, `iso`, `home`, `fit`; `animate` (bool, default false: snap) | |
|
||||
| `ui.start` | `command` | like a toolbar click: starts the sketch tool or dialog for the command |
|
||||
| `ui.click` | `x`, `y` (screen points), `button?`: left/right/middle, `shift?`, `ctrl?` | real pointer input |
|
||||
|
||||
Reference in new issue
Block a user