1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
//! Editor theme and styling for egui
//!
//! Provides a modern, flat UI theme based on the official Bevy Feathers design.
//! Colors are derived from OKLCH values in bevy_feathers for perceptual uniformity.
use bevy_egui::egui::{
self, Color32, CornerRadius, FontFamily, FontId, Stroke, TextStyle, Visuals,
};
use std::sync::atomic::{AtomicBool, Ordering};
/// Track if fonts have already been configured (only need to do once)
static FONTS_CONFIGURED: AtomicBool = AtomicBool::new(false);
/// Bevy-inspired dark theme colors and styling
/// Colors derived from official Bevy Feathers OKLCH palette (converted to sRGB)
pub struct EditorTheme;
impl EditorTheme {
// -------------------------------------------------------------------------------
// Background Colors (from Bevy Feathers OKLCH palette)
// -------------------------------------------------------------------------------
/// Main window/tabs background - darker charcoal for modern look
pub const BG_WINDOW: Color32 = Color32::from_rgb(30, 30, 36);
/// Panel content background - slightly lighter than window
pub const BG_PANEL: Color32 = Color32::from_rgb(35, 35, 40);
/// Widget/button background - subtle elevation
pub const BG_WIDGET: Color32 = Color32::from_rgb(42, 42, 48);
/// Selected tab/elevated widget background
pub const BG_SELECTED_TAB: Color32 = Color32::from_rgb(50, 51, 58);
/// Row hover/highlight background
pub const BG_ROW: Color32 = Color32::from_rgb(45, 46, 52);
// -------------------------------------------------------------------------------
// Border Colors (from Bevy Feathers)
// -------------------------------------------------------------------------------
/// Main border - subtle for modern look
pub const BORDER_MAIN: Color32 = Color32::from_rgb(50, 51, 55);
/// Widget/panel border - subtle
pub const BORDER_WIDGET: Color32 = Color32::from_rgb(55, 56, 60);
/// Side panel border
#[allow(dead_code)]
pub const BORDER_PANEL: Color32 = Color32::from_rgb(70, 71, 75);
/// Tree indent line
#[allow(dead_code)]
pub const BORDER_INDENT: Color32 = Color32::from_rgb(80, 81, 85);
// -------------------------------------------------------------------------------
// Accent/Selection Colors (from Bevy Feathers)
// -------------------------------------------------------------------------------
/// Primary accent blue - ACCENT oklcha(0.542, 0.1594, 255.4)
pub const ACCENT_BLUE: Color32 = Color32::from_rgb(45, 130, 209);
/// Selected tree item background (subtle blue tint)
pub const SELECTION_BG: Color32 = Color32::from_rgb(50, 70, 100);
/// Yellow accent (for warnings/highlights)
#[allow(dead_code)]
pub const ACCENT_YELLOW: Color32 = Color32::from_rgb(255, 202, 57);
// -------------------------------------------------------------------------------
// Text Colors (from Bevy Feathers)
// -------------------------------------------------------------------------------
/// Brightest text - LIGHT_GRAY_1 oklcha(0.7607, 0.0014, 286.37)
pub const TEXT_BRIGHT: Color32 = Color32::from_rgb(191, 191, 193);
/// Primary text
pub const TEXT_PRIMARY: Color32 = Color32::from_rgb(185, 185, 187);
/// Standard text
#[allow(dead_code)]
pub const TEXT_STANDARD: Color32 = Color32::from_rgb(175, 175, 177);
/// Field labels
#[allow(dead_code)]
pub const TEXT_LABEL: Color32 = Color32::from_rgb(170, 170, 172);
/// Filter/input text
#[allow(dead_code)]
pub const TEXT_INPUT: Color32 = Color32::from_rgb(165, 165, 167);
/// Input values
#[allow(dead_code)]
pub const TEXT_VALUE: Color32 = Color32::from_rgb(155, 155, 157);
/// Icon strokes
pub const TEXT_ICON: Color32 = Color32::from_rgb(160, 160, 162);
/// Inactive/muted text - LIGHT_GRAY_2 oklcha(0.6106, 0.003, 286.31)
pub const TEXT_MUTED: Color32 = Color32::from_rgb(147, 148, 150);
/// Very muted text
pub const TEXT_DISABLED: Color32 = Color32::from_rgb(120, 120, 122);
/// Active tab text
#[allow(dead_code)]
pub const TEXT_ACTIVE: Color32 = Color32::from_rgb(200, 200, 202);
/// Pure white for selected/active elements - maximum contrast
pub const TEXT_WHITE: Color32 = Color32::WHITE;
// -------------------------------------------------------------------------------
// Semantic Colors (from Bevy Feathers X/Y/Z axis colors)
// -------------------------------------------------------------------------------
/// Error/X-axis - X_AXIS oklcha(0.5232, 0.1404, 13.84) - coral/red
pub const ERROR: Color32 = Color32::from_rgb(156, 82, 92);
/// Success/Y-axis - Y_AXIS oklcha(0.5866, 0.1543, 129.84) - olive/green
#[allow(dead_code)]
pub const SUCCESS: Color32 = Color32::from_rgb(119, 148, 65);
/// Info/Z-axis - Z_AXIS oklcha(0.4847, 0.1249, 253.08) - steel blue
#[allow(dead_code)]
pub const INFO: Color32 = Color32::from_rgb(72, 113, 161);
/// Warning color
pub const WARNING: Color32 = Color32::from_rgb(200, 160, 60);
// -------------------------------------------------------------------------------════════════
// Theme Application
// -------------------------------------------------------------------------------════════════
/// Apply the editor theme to the egui context.
pub fn apply(ctx: &egui::Context) {
let mut style = (*ctx.style()).clone();
let mut visuals = Visuals::dark();
// ───────────────────────────────────────────────────────────────────────
// Window and Panel fills (from Figma)
// ───────────────────────────────────────────────────────────────────────
visuals.window_fill = Self::BG_WINDOW;
visuals.panel_fill = Self::BG_PANEL;
visuals.faint_bg_color = Self::BG_WINDOW;
visuals.extreme_bg_color = Self::BG_WINDOW;
// Flat design - no shadows per Bevy Feathers style
visuals.popup_shadow = egui::Shadow::NONE;
visuals.window_shadow = egui::Shadow::NONE;
// ───────────────────────────────────────────────────────────────────────
// Widget styling - Non-interactive (labels, static elements)
// ───────────────────────────────────────────────────────────────────────
visuals.widgets.noninteractive.bg_fill = Self::BG_WIDGET;
visuals.widgets.noninteractive.weak_bg_fill = Self::BG_PANEL;
visuals.widgets.noninteractive.fg_stroke = Stroke::new(1.0, Self::TEXT_MUTED);
visuals.widgets.noninteractive.bg_stroke = Stroke::new(0.5, Self::BORDER_WIDGET);
visuals.widgets.noninteractive.corner_radius = CornerRadius::same(5);
// ───────────────────────────────────────────────────────────────────────
// Widget styling - Inactive (interactive but not hovered)
// ───────────────────────────────────────────────────────────────────────
visuals.widgets.inactive.bg_fill = Self::BG_WIDGET;
visuals.widgets.inactive.weak_bg_fill = Self::BG_PANEL;
visuals.widgets.inactive.fg_stroke = Stroke::new(1.0, Self::TEXT_PRIMARY);
visuals.widgets.inactive.bg_stroke = Stroke::new(0.5, Self::BORDER_WIDGET);
visuals.widgets.inactive.corner_radius = CornerRadius::same(5);
// ───────────────────────────────────────────────────────────────────────
// Widget styling - Hovered
// ───────────────────────────────────────────────────────────────────────
visuals.widgets.hovered.bg_fill = Self::BG_SELECTED_TAB;
visuals.widgets.hovered.weak_bg_fill = Self::BG_ROW;
visuals.widgets.hovered.fg_stroke = Stroke::new(1.0, Self::TEXT_BRIGHT);
visuals.widgets.hovered.bg_stroke = Stroke::new(0.5, Self::BORDER_WIDGET);
visuals.widgets.hovered.corner_radius = CornerRadius::same(5);
// ───────────────────────────────────────────────────────────────────────
// Widget styling - Active (being clicked/dragged)
// Solid accent blue background, clean with no border
// Pure white text for maximum readability over blue
// ───────────────────────────────────────────────────────────────────────
visuals.widgets.active.bg_fill = Self::ACCENT_BLUE;
visuals.widgets.active.weak_bg_fill = Self::ACCENT_BLUE;
visuals.widgets.active.fg_stroke = Stroke::new(1.5, Self::TEXT_WHITE);
visuals.widgets.active.bg_stroke = Stroke::NONE;
visuals.widgets.active.corner_radius = CornerRadius::same(5);
// ───────────────────────────────────────────────────────────────────────
// Widget styling - Open (dropdown open, etc.)
// Pure white text for readability
// ───────────────────────────────────────────────────────────────────────
visuals.widgets.open.bg_fill = Self::BG_SELECTED_TAB;
visuals.widgets.open.weak_bg_fill = Self::BG_ROW;
visuals.widgets.open.fg_stroke = Stroke::new(1.5, Self::TEXT_WHITE);
visuals.widgets.open.bg_stroke = Stroke::new(0.5, Self::ACCENT_BLUE);
visuals.widgets.open.corner_radius = CornerRadius::same(5);
// ───────────────────────────────────────────────────────────────────────
// Selection (text selection, selected buttons, etc.)
// Solid accent blue per Figma (background: #206EC9), white text
// ───────────────────────────────────────────────────────────────────────
visuals.selection.bg_fill = Self::ACCENT_BLUE;
visuals.selection.stroke = Stroke::new(1.0, Self::TEXT_WHITE);
// ───────────────────────────────────────────────────────────────────────
// Window/menu rounding (from Figma: 6-8px for panels, 4-5px for items)
// ───────────────────────────────────────────────────────────────────────
visuals.window_corner_radius = CornerRadius::same(6);
visuals.menu_corner_radius = CornerRadius::same(5);
// ───────────────────────────────────────────────────────────────────────
// Misc colors
// ───────────────────────────────────────────────────────────────────────
visuals.hyperlink_color = Self::ACCENT_BLUE;
visuals.warn_fg_color = Self::WARNING;
visuals.error_fg_color = Self::ERROR;
// Striped backgrounds (for tables, lists)
visuals.striped = true;
// ───────────────────────────────────────────────────────────────────────
// Spacing (adjusted for proportional fonts)
// ───────────────────────────────────────────────────────────────────────
style.spacing.item_spacing = egui::vec2(6.0, 3.0);
style.spacing.button_padding = egui::vec2(8.0, 4.0);
style.spacing.indent = 16.0;
style.spacing.interact_size = egui::vec2(40.0, 20.0);
style.spacing.slider_width = 100.0;
style.spacing.combo_width = 120.0;
style.spacing.scroll = egui::style::ScrollStyle {
bar_width: 8.0,
handle_min_length: 20.0,
bar_inner_margin: 2.0,
bar_outer_margin: 2.0,
..Default::default()
};
// ───────────────────────────────────────────────────────────────────────
// Apply
// ───────────────────────────────────────────────────────────────────────
style.visuals = visuals;
ctx.set_style(style);
// Configure fonts (only once per session)
Self::configure_fonts(ctx);
}
/// Configure fonts for a clean, modern UI aesthetic.
///
/// Uses proportional fonts for better readability in UI elements,
/// while keeping monospace available for code display.
fn configure_fonts(ctx: &egui::Context) {
// Only configure fonts once
if FONTS_CONFIGURED.swap(true, Ordering::SeqCst) {
return;
}
// Configure text styles to use proportional fonts for modern look
let mut style = (*ctx.style()).clone();
// Use proportional fonts for UI text (modern, readable)
style
.text_styles
.insert(TextStyle::Body, FontId::new(13.0, FontFamily::Proportional));
style.text_styles.insert(
TextStyle::Small,
FontId::new(11.0, FontFamily::Proportional),
);
style.text_styles.insert(
TextStyle::Button,
FontId::new(13.0, FontFamily::Proportional),
);
style.text_styles.insert(
TextStyle::Heading,
FontId::new(15.0, FontFamily::Proportional),
);
// Keep Monospace style available for code display
style.text_styles.insert(
TextStyle::Monospace,
FontId::new(12.0, FontFamily::Monospace),
);
ctx.set_style(style);
}
// -------------------------------------------------------------------------------════════════
// Helper Methods
// -------------------------------------------------------------------------------════════════
/// Get a frame style for side panels (subtle borders)
#[allow(dead_code)]
pub fn panel_frame() -> egui::Frame {
egui::Frame {
fill: Self::BG_PANEL,
inner_margin: egui::Margin::same(6),
outer_margin: egui::Margin::ZERO,
stroke: Stroke::new(0.5, Self::BORDER_MAIN),
corner_radius: CornerRadius::same(6),
shadow: egui::Shadow::NONE,
}
}
/// Get a frame style for floating windows
#[allow(dead_code)]
pub fn window_frame() -> egui::Frame {
egui::Frame {
fill: Self::BG_WINDOW,
inner_margin: egui::Margin::same(8),
outer_margin: egui::Margin::same(4),
stroke: Stroke::new(0.5, Self::BORDER_WIDGET),
corner_radius: CornerRadius::same(8),
shadow: egui::Shadow::NONE,
}
}
/// Get a frame style for toolbars (top bars)
#[allow(dead_code)]
pub fn toolbar_frame() -> egui::Frame {
egui::Frame {
fill: Self::BG_WINDOW,
inner_margin: egui::Margin::symmetric(8, 4),
outer_margin: egui::Margin::ZERO,
stroke: Stroke::new(0.5, Self::BORDER_MAIN),
corner_radius: CornerRadius::ZERO,
shadow: egui::Shadow::NONE,
}
}
/// Get a frame style for collapsible component headers (like Transform in Figma)
#[allow(dead_code)]
pub fn component_header_frame() -> egui::Frame {
egui::Frame {
fill: Self::BG_WIDGET,
inner_margin: egui::Margin::symmetric(8, 5),
outer_margin: egui::Margin::ZERO,
stroke: Stroke::NONE,
corner_radius: CornerRadius {
nw: 5,
ne: 5,
sw: 0,
se: 0,
},
shadow: egui::Shadow::NONE,
}
}
}