Skip to main content

libsurfer/
config.rs

1use config::builder::DefaultState;
2use config::{Config, ConfigBuilder};
3#[cfg(not(target_arch = "wasm32"))]
4use config::{Environment, File};
5use derive_more::{Display, FromStr};
6#[cfg(not(target_arch = "wasm32"))]
7use directories::ProjectDirs;
8use ecolor::Color32;
9use enum_iterator::Sequence;
10use epaint::{PathStroke, Stroke};
11use eyre::Report;
12use eyre::{Context, Result};
13use serde::de;
14use serde::{Deserialize, Deserializer, Serialize};
15use std::collections::HashMap;
16#[cfg(not(target_arch = "wasm32"))]
17use std::path::{Path, PathBuf};
18use std::sync::LazyLock;
19use surver::SurverConfig;
20
21use crate::hierarchy::{HierarchyStyle, ParameterDisplayLocation};
22use crate::keyboard_shortcuts::{SurferShortcuts, deserialize_shortcuts};
23use crate::mousegestures::GestureZones;
24use crate::time::TimeFormat;
25use crate::wave_container::VariableMeta;
26use crate::{clock_highlighting::ClockHighlightType, variable_name_type::VariableNameType};
27use surfer_translation_types::VariableEncoding;
28
29macro_rules! theme {
30    ($name:expr) => {
31        (
32            $name,
33            include_str!(concat!("../../themes/", $name, ".toml")),
34        )
35    };
36}
37
38/// Built-in theme names and their corresponding embedded content
39static BUILTIN_THEMES: LazyLock<HashMap<&'static str, &'static str>> = LazyLock::new(|| {
40    HashMap::from([
41        theme!("dark+"),
42        theme!("dark-high-contrast"),
43        theme!("ibm"),
44        theme!("light+"),
45        theme!("light-high-contrast"),
46        ("okabe/ito", include_str!("../../themes/okabe-ito.toml")),
47        theme!("petroff-dark"),
48        theme!("petroff-light"),
49        ("Rosé Pine", include_str!("../../themes/rose-pine.toml")),
50        (
51            "Rosé Pine Moon",
52            include_str!("../../themes/rose-pine-moon.toml"),
53        ),
54        (
55            "Rosé Pine Dawn",
56            include_str!("../../themes/rose-pine-dawn.toml"),
57        ),
58        theme!("solarized"),
59    ])
60});
61
62#[cfg(not(target_arch = "wasm32"))]
63pub static PROJECT_DIR: LazyLock<Option<ProjectDirs>> =
64    LazyLock::new(|| ProjectDirs::from("org", "surfer-project", "surfer"));
65#[cfg(not(target_arch = "wasm32"))]
66const OLD_CONFIG_FILE: &str = "surfer.toml";
67#[cfg(not(target_arch = "wasm32"))]
68const CONFIG_FILE: &str = "config.toml";
69#[cfg(not(target_arch = "wasm32"))]
70const THEMES_DIR: &str = "themes";
71#[cfg(not(target_arch = "wasm32"))]
72pub const LOCAL_DIR: &str = ".surfer";
73
74/// Select the function of the arrow keys
75#[derive(Clone, Copy, Debug, Deserialize, Display, FromStr, PartialEq, Eq, Sequence, Serialize)]
76pub enum ArrowKeyBindings {
77    /// The left/right arrow keys step to the next edge
78    Edge,
79
80    /// The left/right arrow keys scroll the viewport left/right
81    Scroll,
82}
83
84#[derive(Clone, Copy, Debug, Deserialize, Display, FromStr, PartialEq, Eq, Sequence, Serialize)]
85pub enum TransitionValue {
86    /// Transition value is the previous value
87    Previous,
88    /// Transition value is the next value
89    Next,
90    /// Transition value is both previous and next value
91    Both,
92}
93
94/// Select the function when dragging with primary mouse button
95#[derive(Debug, Deserialize, Display, PartialEq, Eq, Sequence, Serialize, Clone, Copy)]
96pub enum PrimaryMouseDrag {
97    /// The left/right arrow keys step to the next edge
98    #[display("Measure time")]
99    Measure,
100
101    /// The left/right arrow keys scroll the viewport left/right
102    #[display("Move cursor")]
103    Cursor,
104}
105
106#[derive(Debug, Deserialize, Display, PartialEq, Eq, Sequence, Serialize, Clone, Copy)]
107pub enum AutoLoad {
108    Always,
109    Never,
110    Ask,
111}
112
113impl AutoLoad {
114    #[must_use]
115    pub fn from_bool(auto_load: bool) -> Self {
116        if auto_load {
117            AutoLoad::Always
118        } else {
119            AutoLoad::Never
120        }
121    }
122}
123
124#[derive(Debug, Deserialize)]
125pub struct SurferConfig {
126    pub layout: SurferLayout,
127    #[serde(deserialize_with = "deserialize_theme")]
128    pub theme: SurferTheme,
129    /// Mouse gesture configurations. Color and linewidth are configured in the theme using [`SurferTheme::gesture`].
130    pub gesture: SurferGesture,
131    pub behavior: SurferBehavior,
132    /// Time stamp format
133    pub default_time_format: TimeFormat,
134    pub default_variable_name_type: VariableNameType,
135    default_clock_highlight_type: ClockHighlightType,
136    /// Distance in pixels for cursor snap
137    pub snap_distance: f32,
138    /// Maximum size of the undo stack
139    pub undo_stack_size: usize,
140    /// Reload changed waves
141    autoreload_files: AutoLoad,
142    /// Load state file
143    autoload_sibling_state_files: AutoLoad,
144    /// WCP Configuration
145    pub wcp: WcpConfig,
146    /// HTTP Server Configuration
147    pub server: SurverConfig,
148    /// Animation time for UI elements in seconds
149    pub animation_time: f32,
150    /// UI animation enabled
151    pub animation_enabled: bool,
152    /// Maximum URL length for remote connections.
153    /// Should only be changed in case you are behind a proxy that limits the URL length
154    pub max_url_length: u16,
155    /// Keyboard shortcuts
156    #[serde(deserialize_with = "deserialize_shortcuts")]
157    pub shortcuts: SurferShortcuts,
158}
159
160impl SurferConfig {
161    #[must_use]
162    pub fn default_clock_highlight_type(&self) -> ClockHighlightType {
163        self.default_clock_highlight_type
164    }
165
166    #[must_use]
167    pub fn autoload_sibling_state_files(&self) -> AutoLoad {
168        self.autoload_sibling_state_files
169    }
170
171    #[must_use]
172    pub fn autoreload_files(&self) -> AutoLoad {
173        self.autoreload_files
174    }
175
176    #[must_use]
177    pub fn animation_enabled(&self) -> bool {
178        self.animation_enabled
179    }
180}
181
182#[derive(Debug, Deserialize)]
183pub struct SurferLayout {
184    /// Flag to show/hide the hierarchy view
185    show_hierarchy: bool,
186    /// Flag to show/hide the menu
187    show_menu: bool,
188    /// Flag to show/hide toolbar
189    show_toolbar: bool,
190    /// Flag to show/hide tick lines
191    show_ticks: bool,
192    /// Flag to show/hide tooltip for variables
193    show_tooltip: bool,
194    /// Flag to show/hide tooltip for scopes
195    show_scope_tooltip: bool,
196    /// Flag to show/hide the overview
197    show_overview: bool,
198    /// Flag to show/hide the statusbar
199    show_statusbar: bool,
200    /// Flag to show/hide the indices of variables in the variable list
201    show_variable_indices: bool,
202    /// Flag to show/hide the variable direction icon
203    show_variable_direction: bool,
204    /// Flag to show/hide a default timeline
205    show_default_timeline: bool,
206    /// Flag to show/hide empty scopes
207    show_empty_scopes: bool,
208    /// Flag to show/hide scope and variable type icons in the hierarchy
209    show_hierarchy_icons: bool,
210    /// Where to show parameters in the hierarchy
211    parameter_display_location: ParameterDisplayLocation,
212    /// Initial window height
213    pub window_height: usize,
214    /// Initial window width
215    pub window_width: usize,
216    /// Align variable names right
217    align_names_right: bool,
218    /// Set style of hierarchy
219    hierarchy_style: HierarchyStyle,
220    /// Text size in points for values in waves
221    pub waveforms_text_size: f32,
222    /// Line height in points for waves
223    pub waveforms_line_height: f32,
224    /// Line height multiples for higher variables
225    pub waveforms_line_height_multiples: Vec<f32>,
226    /// Line height in points for transaction streams
227    pub transactions_line_height: f32,
228    /// UI zoom factors
229    pub zoom_factors: Vec<f32>,
230    /// Default UI zoom factor
231    pub default_zoom_factor: f32,
232    #[serde(default)]
233    /// Highlight the waveform of the focused item?
234    highlight_focused: bool,
235    /// Move the focus to the newly inserted marker?
236    move_focus_on_inserted_marker: bool,
237    /// Fill high values in boolean waveforms
238    #[serde(default = "default_true")]
239    fill_high_values: bool,
240    /// Dinotrace drawing style (thick upper line for all-ones, no upper line for all-zeros)
241    #[serde(default)]
242    use_dinotrace_style: bool,
243    /// Value to display when cursor is on a transition
244    #[serde(default = "default_next")]
245    transition_value: TransitionValue,
246}
247
248fn default_true() -> bool {
249    true
250}
251
252fn default_next() -> TransitionValue {
253    TransitionValue::Next
254}
255
256impl SurferLayout {
257    #[must_use]
258    pub fn show_hierarchy(&self) -> bool {
259        self.show_hierarchy
260    }
261    #[must_use]
262    pub fn show_menu(&self) -> bool {
263        self.show_menu
264    }
265    #[must_use]
266    pub fn show_ticks(&self) -> bool {
267        self.show_ticks
268    }
269    #[must_use]
270    pub fn show_tooltip(&self) -> bool {
271        self.show_tooltip
272    }
273    #[must_use]
274    pub fn show_scope_tooltip(&self) -> bool {
275        self.show_scope_tooltip
276    }
277    #[must_use]
278    pub fn show_default_timeline(&self) -> bool {
279        self.show_default_timeline
280    }
281    #[must_use]
282    pub fn show_toolbar(&self) -> bool {
283        self.show_toolbar
284    }
285    #[must_use]
286    pub fn show_overview(&self) -> bool {
287        self.show_overview
288    }
289    #[must_use]
290    pub fn show_statusbar(&self) -> bool {
291        self.show_statusbar
292    }
293    #[must_use]
294    pub fn align_names_right(&self) -> bool {
295        self.align_names_right
296    }
297    #[must_use]
298    pub fn show_variable_indices(&self) -> bool {
299        self.show_variable_indices
300    }
301    #[must_use]
302    pub fn show_variable_direction(&self) -> bool {
303        self.show_variable_direction
304    }
305    #[must_use]
306    pub fn default_zoom_factor(&self) -> f32 {
307        self.default_zoom_factor
308    }
309    #[must_use]
310    pub fn show_empty_scopes(&self) -> bool {
311        self.show_empty_scopes
312    }
313    #[must_use]
314    pub fn show_hierarchy_icons(&self) -> bool {
315        self.show_hierarchy_icons
316    }
317    #[must_use]
318    pub fn parameter_display_location(&self) -> ParameterDisplayLocation {
319        self.parameter_display_location
320    }
321    #[must_use]
322    pub fn highlight_focused(&self) -> bool {
323        self.highlight_focused
324    }
325    #[must_use]
326    pub fn move_focus_on_inserted_marker(&self) -> bool {
327        self.move_focus_on_inserted_marker
328    }
329    #[must_use]
330    pub fn fill_high_values(&self) -> bool {
331        self.fill_high_values
332    }
333    #[must_use]
334    pub fn hierarchy_style(&self) -> HierarchyStyle {
335        self.hierarchy_style
336    }
337    #[must_use]
338    pub fn use_dinotrace_style(&self) -> bool {
339        self.use_dinotrace_style
340    }
341    #[must_use]
342    pub fn transition_value(&self) -> TransitionValue {
343        self.transition_value
344    }
345}
346
347#[derive(Debug, Deserialize)]
348pub struct SurferBehavior {
349    /// Keep or remove variables if unavailable during reload
350    pub keep_during_reload: bool,
351    /// Select the functionality bound to the arrow keys
352    pub arrow_key_bindings: ArrowKeyBindings,
353    /// Whether dragging with primary mouse button will measure time or move cursor
354    /// (press shift for the other)
355    primary_button_drag_behavior: PrimaryMouseDrag,
356}
357
358impl SurferBehavior {
359    #[must_use]
360    pub fn primary_button_drag_behavior(&self) -> PrimaryMouseDrag {
361        self.primary_button_drag_behavior
362    }
363
364    #[must_use]
365    pub fn arrow_key_bindings(&self) -> ArrowKeyBindings {
366        self.arrow_key_bindings
367    }
368}
369
370#[derive(Debug, Deserialize)]
371/// Mouse gesture configurations. Color and linewidth are configured in the theme using [`SurferTheme::gesture`].
372pub struct SurferGesture {
373    /// Size of the overlay help
374    pub size: f32,
375    /// (Squared) minimum distance to move to remove the overlay help and perform gesture
376    pub deadzone: f32,
377    /// Circle radius for background as a factor of size/2
378    pub background_radius: f32,
379    /// Gamma factor for background circle, between 0 (opaque) and 1 (transparent)
380    pub background_gamma: f32,
381    /// Mapping between the eight directions and actions
382    pub mapping: GestureZones,
383}
384
385#[derive(Clone, Debug, Deserialize)]
386pub struct SurferLineStyle {
387    #[serde(deserialize_with = "deserialize_hex_color")]
388    pub color: Color32,
389    pub width: f32,
390}
391
392impl From<SurferLineStyle> for Stroke {
393    fn from(style: SurferLineStyle) -> Self {
394        Stroke {
395            color: style.color,
396            width: style.width,
397        }
398    }
399}
400
401impl From<&SurferLineStyle> for Stroke {
402    fn from(style: &SurferLineStyle) -> Self {
403        Stroke {
404            color: style.color,
405            width: style.width,
406        }
407    }
408}
409
410impl From<&SurferLineStyle> for PathStroke {
411    fn from(style: &SurferLineStyle) -> Self {
412        PathStroke::new(style.width, style.color)
413    }
414}
415
416#[derive(Debug, Deserialize)]
417/// Tick mark configuration
418pub struct SurferTicks {
419    /// 0 to 1, where 1 means as many ticks that can fit without overlap
420    pub density: f32,
421    /// Line style to use for ticks
422    pub style: SurferLineStyle,
423}
424
425#[derive(Debug, Deserialize)]
426pub struct SurferRelationArrow {
427    /// Arrow line style
428    pub style: SurferLineStyle,
429
430    /// Arrowhead angle in degrees
431    pub head_angle: f32,
432
433    /// Arrowhead length
434    pub head_length: f32,
435}
436
437#[derive(Debug, Deserialize)]
438pub struct SurferTheme {
439    /// Color used for text across the UI
440    #[serde(deserialize_with = "deserialize_hex_color")]
441    pub foreground: Color32,
442    #[serde(deserialize_with = "deserialize_hex_color")]
443    /// Color of borders between UI elements
444    pub border_color: Color32,
445    /// Color used for text across the markers
446    #[serde(deserialize_with = "deserialize_hex_color")]
447    pub alt_text_color: Color32,
448    /// Colors used for the background and text of the wave view
449    pub canvas_colors: ThemeColorTriple,
450    /// Colors used for most UI elements not on the variable canvas
451    pub primary_ui_color: ThemeColorPair,
452    /// Colors used for the variable and value list, as well as secondary elements
453    /// like text fields
454    pub secondary_ui_color: ThemeColorPair,
455    /// Color used for selected ui elements such as the currently selected hierarchy
456    pub selected_elements_colors: ThemeColorPair,
457
458    pub accent_info: ThemeColorPair,
459    pub accent_warn: ThemeColorPair,
460    pub accent_error: ThemeColorPair,
461
462    ///  Line style for cursor
463    pub cursor: SurferLineStyle,
464
465    /// Line style for mouse gesture lines
466    pub gesture: SurferLineStyle,
467
468    /// Line style for measurement lines
469    pub measure: SurferLineStyle,
470
471    ///  Line style for clock highlight lines
472    pub clock_highlight_line: SurferLineStyle,
473    #[serde(deserialize_with = "deserialize_hex_color")]
474    pub clock_highlight_cycle: Color32,
475    /// Draw arrows on rising clock edges
476    pub clock_rising_marker: bool,
477
478    #[serde(deserialize_with = "deserialize_hex_color")]
479    /// Default variable color
480    pub variable_default: Color32,
481    #[serde(deserialize_with = "deserialize_hex_color")]
482    /// Color used for high-impedance variables
483    pub variable_highimp: Color32,
484    #[serde(deserialize_with = "deserialize_hex_color")]
485    /// Color used for undefined variables
486    pub variable_undef: Color32,
487    #[serde(deserialize_with = "deserialize_hex_color")]
488    /// Color used for don't-care variables
489    pub variable_dontcare: Color32,
490    #[serde(deserialize_with = "deserialize_hex_color")]
491    /// Color used for weak variables
492    pub variable_weak: Color32,
493    #[serde(deserialize_with = "deserialize_hex_color")]
494    /// Color used for constant variables (parameters)
495    pub variable_parameter: Color32,
496    #[serde(deserialize_with = "deserialize_hex_color")]
497    /// Default transaction color
498    pub transaction_default: Color32,
499    // Relation arrows of transactions
500    pub relation_arrow: SurferRelationArrow,
501    #[serde(deserialize_with = "deserialize_hex_color")]
502    /// Color used for constant variables (parameters)
503    pub variable_event: Color32,
504
505    /// Opacity with which variable backgrounds are drawn. 0 is fully transparent and 1 is fully
506    /// opaque.
507    pub waveform_opacity: f32,
508    /// Opacity of variable backgrounds for wide signals (signals with more than one bit)
509    #[serde(default)]
510    pub wide_opacity: f32,
511
512    #[serde(default = "default_colors", deserialize_with = "deserialize_color_map")]
513    pub colors: HashMap<String, Color32>,
514    #[serde(deserialize_with = "deserialize_hex_color")]
515    pub highlight_background: Color32,
516
517    /// Variable line width
518    pub linewidth: f32,
519
520    /// Variable line width for accented variables
521    pub thick_linewidth: f32,
522
523    /// Vector transition max width
524    pub vector_transition_width: f32,
525
526    /// Number of lines using standard background before changing to
527    /// alternate background and so on, set to zero to disable
528    pub alt_frequency: usize,
529
530    /// Viewport separator line
531    pub viewport_separator: SurferLineStyle,
532
533    // Drag hint and threshold parameters
534    #[serde(deserialize_with = "deserialize_hex_color")]
535    pub drag_hint_color: Color32,
536    pub drag_hint_width: f32,
537    pub drag_threshold: f32,
538
539    /// Tick information
540    pub ticks: SurferTicks,
541
542    /// List of theme names
543    #[serde(default = "Vec::new")]
544    pub theme_names: Vec<String>,
545
546    /// Icons for scope types in the hierarchy view
547    #[serde(default)]
548    pub scope_icons: ScopeIcons,
549
550    /// Icons for variable types in the hierarchy view
551    #[serde(default)]
552    pub variable_icons: VariableIcons,
553}
554
555/// Colors for different scope type icons in the hierarchy view.
556#[derive(Clone, Debug, Deserialize)]
557#[serde(default)]
558pub struct ScopeIconColors {
559    #[serde(deserialize_with = "deserialize_hex_color")]
560    pub module: Color32,
561    #[serde(deserialize_with = "deserialize_hex_color")]
562    pub task: Color32,
563    #[serde(deserialize_with = "deserialize_hex_color")]
564    pub function: Color32,
565    #[serde(deserialize_with = "deserialize_hex_color")]
566    pub begin: Color32,
567    #[serde(deserialize_with = "deserialize_hex_color")]
568    pub fork: Color32,
569    #[serde(deserialize_with = "deserialize_hex_color")]
570    pub generate: Color32,
571    #[serde(rename = "struct", deserialize_with = "deserialize_hex_color")]
572    pub struct_: Color32,
573    #[serde(deserialize_with = "deserialize_hex_color")]
574    pub union: Color32,
575    #[serde(deserialize_with = "deserialize_hex_color")]
576    pub class: Color32,
577    #[serde(deserialize_with = "deserialize_hex_color")]
578    pub interface: Color32,
579    #[serde(deserialize_with = "deserialize_hex_color")]
580    pub package: Color32,
581    #[serde(deserialize_with = "deserialize_hex_color")]
582    pub program: Color32,
583    #[serde(deserialize_with = "deserialize_hex_color")]
584    pub vhdl_architecture: Color32,
585    #[serde(deserialize_with = "deserialize_hex_color")]
586    pub vhdl_procedure: Color32,
587    #[serde(deserialize_with = "deserialize_hex_color")]
588    pub vhdl_function: Color32,
589    #[serde(deserialize_with = "deserialize_hex_color")]
590    pub vhdl_record: Color32,
591    #[serde(deserialize_with = "deserialize_hex_color")]
592    pub vhdl_process: Color32,
593    #[serde(deserialize_with = "deserialize_hex_color")]
594    pub vhdl_block: Color32,
595    #[serde(deserialize_with = "deserialize_hex_color")]
596    pub vhdl_for_generate: Color32,
597    #[serde(deserialize_with = "deserialize_hex_color")]
598    pub vhdl_if_generate: Color32,
599    #[serde(deserialize_with = "deserialize_hex_color")]
600    pub vhdl_generate: Color32,
601    #[serde(deserialize_with = "deserialize_hex_color")]
602    pub vhdl_package: Color32,
603    #[serde(deserialize_with = "deserialize_hex_color")]
604    pub ghw_generic: Color32,
605    #[serde(deserialize_with = "deserialize_hex_color")]
606    pub vhdl_array: Color32,
607    #[serde(deserialize_with = "deserialize_hex_color")]
608    pub unknown: Color32,
609    #[serde(deserialize_with = "deserialize_hex_color")]
610    pub clocking: Color32,
611    #[serde(deserialize_with = "deserialize_hex_color")]
612    pub sv_array: Color32,
613}
614
615impl Default for ScopeIconColors {
616    fn default() -> Self {
617        Self {
618            module: Color32::from_rgb(0x4F, 0xC3, 0xF7), // Light Blue
619            task: Color32::from_rgb(0xFF, 0xB7, 0x4D),   // Orange
620            function: Color32::from_rgb(0xBA, 0x68, 0xC8), // Purple
621            begin: Color32::from_rgb(0x81, 0xC7, 0x84),  // Green
622            fork: Color32::from_rgb(0xFF, 0x80, 0x80),   // Red
623            generate: Color32::from_rgb(0x64, 0xB5, 0xF6), // Blue
624            struct_: Color32::from_rgb(0x4D, 0xD0, 0xE1), // Cyan
625            union: Color32::from_rgb(0x4D, 0xD0, 0xE1),  // Cyan
626            class: Color32::from_rgb(0xF0, 0x62, 0x92),  // Pink
627            interface: Color32::from_rgb(0xAE, 0xD5, 0x81), // Light Green
628            package: Color32::from_rgb(0xFF, 0xD5, 0x4F), // Yellow
629            program: Color32::from_rgb(0xA1, 0x88, 0x7F), // Brown
630            vhdl_architecture: Color32::from_rgb(0x4F, 0xC3, 0xF7), // Light Blue (like module)
631            vhdl_procedure: Color32::from_rgb(0xFF, 0xB7, 0x4D), // Orange (like task)
632            vhdl_function: Color32::from_rgb(0xBA, 0x68, 0xC8), // Purple (like function)
633            vhdl_record: Color32::from_rgb(0x4D, 0xD0, 0xE1), // Cyan (like struct)
634            vhdl_process: Color32::from_rgb(0x81, 0xC7, 0x84), // Green (like begin)
635            vhdl_block: Color32::from_rgb(0x90, 0xA4, 0xAE), // Blue Grey
636            vhdl_for_generate: Color32::from_rgb(0x64, 0xB5, 0xF6), // Blue (like generate)
637            vhdl_if_generate: Color32::from_rgb(0x64, 0xB5, 0xF6), // Blue (like generate)
638            vhdl_generate: Color32::from_rgb(0x64, 0xB5, 0xF6), // Blue (like generate)
639            vhdl_package: Color32::from_rgb(0xFF, 0xD5, 0x4F), // Yellow (like package)
640            ghw_generic: Color32::from_rgb(0xB0, 0xBE, 0xC5), // Blue Grey Light
641            vhdl_array: Color32::from_rgb(0xCE, 0x93, 0xD8), // Light Purple
642            clocking: Color32::from_rgb(0xF0, 0x62, 0x92), // Pink (like class)
643            sv_array: Color32::from_rgb(0xCE, 0x93, 0xD8), // Light Purple (like vhdl_array)
644            unknown: Color32::from_rgb(0x9E, 0x9E, 0x9E), // Grey
645        }
646    }
647}
648
649/// Icons for different scope types in the hierarchy view.
650/// Each field maps to a wellen::ScopeType and contains a Remix icon string.
651#[derive(Clone, Debug, Deserialize)]
652#[serde(default)]
653pub struct ScopeIcons {
654    // Verilog/SystemVerilog scope types
655    pub module: String,
656    pub task: String,
657    pub function: String,
658    pub begin: String,
659    pub fork: String,
660    pub generate: String,
661    #[serde(rename = "struct")]
662    pub struct_: String,
663    pub union: String,
664    pub class: String,
665    pub interface: String,
666    pub package: String,
667    pub program: String,
668    // VHDL scope types
669    pub vhdl_architecture: String,
670    pub vhdl_procedure: String,
671    pub vhdl_function: String,
672    pub vhdl_record: String,
673    pub vhdl_process: String,
674    pub vhdl_block: String,
675    pub vhdl_for_generate: String,
676    pub vhdl_if_generate: String,
677    pub vhdl_generate: String,
678    pub vhdl_package: String,
679    pub ghw_generic: String,
680    pub vhdl_array: String,
681    pub unknown: String,
682    pub clocking: String,
683    pub sv_array: String,
684    /// Colors for scope icons
685    #[serde(default)]
686    pub colors: ScopeIconColors,
687}
688
689impl Default for ScopeIcons {
690    fn default() -> Self {
691        use egui_remixicon::icons;
692        Self {
693            // Verilog/SystemVerilog scope types
694            module: icons::CPU_LINE.to_string(),
695            task: icons::TASK_LINE.to_string(),
696            function: icons::BRACES_LINE.to_string(),
697            begin: icons::CODE_BOX_LINE.to_string(),
698            fork: icons::GIT_BRANCH_LINE.to_string(),
699            generate: icons::REPEAT_LINE.to_string(),
700            struct_: icons::TABLE_LINE.to_string(),
701            union: icons::MERGE_CELLS_HORIZONTAL.to_string(),
702            class: icons::TABLE_LINE.to_string(),
703            interface: icons::PLUG_LINE.to_string(),
704            package: icons::BOX_3_LINE.to_string(),
705            program: icons::FILE_CODE_LINE.to_string(),
706            // VHDL scope types
707            vhdl_architecture: icons::CPU_LINE.to_string(),
708            vhdl_procedure: icons::TERMINAL_LINE.to_string(),
709            vhdl_function: icons::BRACES_LINE.to_string(),
710            vhdl_record: icons::TABLE_LINE.to_string(),
711            vhdl_process: icons::FLASHLIGHT_LINE.to_string(),
712            vhdl_block: icons::CODE_BLOCK.to_string(),
713            vhdl_for_generate: icons::REPEAT_LINE.to_string(),
714            vhdl_if_generate: icons::QUESTION_LINE.to_string(),
715            vhdl_generate: icons::REPEAT_LINE.to_string(),
716            vhdl_package: icons::BOX_3_LINE.to_string(),
717            ghw_generic: icons::SETTINGS_3_LINE.to_string(),
718            vhdl_array: icons::BRACKETS_LINE.to_string(),
719            sv_array: icons::BRACKETS_LINE.to_string(),
720            clocking: icons::TIME_LINE.to_string(),
721            unknown: icons::QUESTION_LINE.to_string(),
722            colors: ScopeIconColors::default(),
723        }
724    }
725}
726
727impl ScopeIcons {
728    /// Returns the icon and color for a given scope type.
729    /// If `scope_type` is `None`, returns the default module icon and color.
730    #[must_use]
731    pub fn get_icon(&self, scope_type: Option<wellen::ScopeType>) -> (&str, Color32) {
732        use wellen::ScopeType;
733        match scope_type {
734            None => (&self.module, self.colors.module),
735            Some(st) => match st {
736                ScopeType::Module => (&self.module, self.colors.module),
737                ScopeType::Task => (&self.task, self.colors.task),
738                ScopeType::Function => (&self.function, self.colors.function),
739                ScopeType::Begin => (&self.begin, self.colors.begin),
740                ScopeType::Fork => (&self.fork, self.colors.fork),
741                ScopeType::Generate => (&self.generate, self.colors.generate),
742                ScopeType::Struct => (&self.struct_, self.colors.struct_),
743                ScopeType::Union => (&self.union, self.colors.union),
744                ScopeType::Class => (&self.class, self.colors.class),
745                ScopeType::Interface => (&self.interface, self.colors.interface),
746                ScopeType::Package => (&self.package, self.colors.package),
747                ScopeType::Program => (&self.program, self.colors.program),
748                ScopeType::VhdlArchitecture => {
749                    (&self.vhdl_architecture, self.colors.vhdl_architecture)
750                }
751                ScopeType::VhdlProcedure => (&self.vhdl_procedure, self.colors.vhdl_procedure),
752                ScopeType::VhdlFunction => (&self.vhdl_function, self.colors.vhdl_function),
753                ScopeType::VhdlRecord => (&self.vhdl_record, self.colors.vhdl_record),
754                ScopeType::VhdlProcess => (&self.vhdl_process, self.colors.vhdl_process),
755                ScopeType::VhdlBlock => (&self.vhdl_block, self.colors.vhdl_block),
756                ScopeType::VhdlForGenerate => {
757                    (&self.vhdl_for_generate, self.colors.vhdl_for_generate)
758                }
759                ScopeType::VhdlIfGenerate => (&self.vhdl_if_generate, self.colors.vhdl_if_generate),
760                ScopeType::VhdlGenerate => (&self.vhdl_generate, self.colors.vhdl_generate),
761                ScopeType::VhdlPackage => (&self.vhdl_package, self.colors.vhdl_package),
762                ScopeType::GhwGeneric => (&self.ghw_generic, self.colors.ghw_generic),
763                ScopeType::VhdlArray => (&self.vhdl_array, self.colors.vhdl_array),
764                ScopeType::Unknown => (&self.unknown, self.colors.unknown),
765                ScopeType::SvArray => (&self.sv_array, self.colors.sv_array),
766                ScopeType::Clocking => (&self.clocking, self.colors.clocking),
767                _ => (&self.unknown, self.colors.unknown),
768            },
769        }
770    }
771}
772
773/// Colors for different variable type icons in the hierarchy view.
774/// Each field contains a Color32 value for the corresponding variable type.
775#[derive(Clone, Debug, Deserialize)]
776#[serde(default)]
777pub struct VariableIconColors {
778    /// Color for 1-bit wire signals
779    #[serde(deserialize_with = "deserialize_hex_color")]
780    pub wire: Color32,
781    /// Color for multi-bit bus signals
782    #[serde(deserialize_with = "deserialize_hex_color")]
783    pub bus: Color32,
784    /// Color for string variables
785    #[serde(deserialize_with = "deserialize_hex_color")]
786    pub string: Color32,
787    /// Color for event variables
788    #[serde(deserialize_with = "deserialize_hex_color")]
789    pub event: Color32,
790    /// Color for other types (integers, floats, enums)
791    #[serde(deserialize_with = "deserialize_hex_color")]
792    pub other: Color32,
793}
794
795impl Default for VariableIconColors {
796    fn default() -> Self {
797        Self {
798            wire: Color32::from_rgb(0x81, 0xC7, 0x84),   // Green
799            bus: Color32::from_rgb(0x64, 0xB5, 0xF6),    // Blue
800            string: Color32::from_rgb(0xFF, 0xB7, 0x4D), // Orange
801            event: Color32::from_rgb(0xF0, 0x62, 0x92),  // Pink
802            other: Color32::from_rgb(0xBA, 0x68, 0xC8),  // Purple
803        }
804    }
805}
806
807/// Icons for different variable types in the hierarchy view.
808/// Each field contains a Remix icon string.
809#[derive(Clone, Debug, Deserialize)]
810#[serde(default)]
811pub struct VariableIcons {
812    /// 1-bit wire signals
813    pub wire: String,
814    /// Multi-bit bus signals
815    pub bus: String,
816    /// String variables
817    pub string: String,
818    /// Event variables
819    pub event: String,
820    /// Other types (integers, floats, enums)
821    pub other: String,
822    /// Colors for variable icons
823    #[serde(default)]
824    pub colors: VariableIconColors,
825}
826
827impl Default for VariableIcons {
828    fn default() -> Self {
829        use egui_remixicon::icons;
830        Self {
831            wire: icons::GIT_COMMIT_LINE.to_string(),
832            bus: icons::BRACKETS_LINE.to_string(),
833            string: icons::TEXT.to_string(),
834            event: icons::ARROW_UP_LONG_LINE.to_string(),
835            other: icons::NUMBERS_LINE.to_string(),
836            colors: VariableIconColors::default(),
837        }
838    }
839}
840
841impl VariableIcons {
842    /// Returns the icon and color for a given variable meta.
843    /// If `meta` is `None`, returns the default "other" icon and color.
844    #[must_use]
845    pub fn get_icon(&self, meta: Option<&VariableMeta>) -> (&str, Color32) {
846        let Some(meta) = meta else {
847            return (&self.other, self.colors.other);
848        };
849
850        match meta.encoding {
851            VariableEncoding::String => (&self.string, self.colors.string),
852            VariableEncoding::Event => (&self.event, self.colors.event),
853            VariableEncoding::Real => (&self.other, self.colors.other),
854            VariableEncoding::BitVector => match meta.num_bits {
855                Some(1) => (&self.wire, self.colors.wire),
856                Some(n) if n > 1 => (&self.bus, self.colors.bus),
857                _ => (&self.other, self.colors.other),
858            },
859        }
860    }
861}
862
863fn get_luminance(color: Color32) -> f32 {
864    let rg = if color.r() < 10 {
865        f32::from(color.r()) / 3294.0
866    } else {
867        (f32::from(color.r()) / 269.0 + 0.0513).powf(2.4)
868    };
869    let gg = if color.g() < 10 {
870        f32::from(color.g()) / 3294.0
871    } else {
872        (f32::from(color.g()) / 269.0 + 0.0513).powf(2.4)
873    };
874    let bg = if color.b() < 10 {
875        f32::from(color.b()) / 3294.0
876    } else {
877        (f32::from(color.b()) / 269.0 + 0.0513).powf(2.4)
878    };
879    0.2126 * rg + 0.7152 * gg + 0.0722 * bg
880}
881
882impl SurferTheme {
883    #[must_use]
884    pub fn get_color(&self, color: &str) -> Option<Color32> {
885        self.colors.get(color).copied()
886    }
887
888    #[must_use]
889    pub fn get_best_text_color(&self, backgroundcolor: Color32) -> Color32 {
890        // Based on https://ux.stackexchange.com/questions/82056/how-to-measure-the-contrast-between-any-given-color-and-white
891
892        // Compute luminance
893        let l_foreground = get_luminance(self.foreground);
894        let l_alt_text_color = get_luminance(self.alt_text_color);
895        let l_background = get_luminance(backgroundcolor);
896
897        // Compute contrast ratio
898        let mut cr_foreground = (l_foreground + 0.05) / (l_background + 0.05);
899        cr_foreground = cr_foreground.max(1. / cr_foreground);
900        let mut cr_alt_text_color = (l_alt_text_color + 0.05) / (l_background + 0.05);
901        cr_alt_text_color = cr_alt_text_color.max(1. / cr_alt_text_color);
902
903        // Return color with highest contrast
904        if cr_foreground > cr_alt_text_color {
905            self.foreground
906        } else {
907            self.alt_text_color
908        }
909    }
910
911    fn generate_defaults(
912        theme_name: Option<&String>,
913    ) -> (ConfigBuilder<DefaultState>, Vec<String>) {
914        let default_theme = String::from(include_str!("../../default_theme.toml"));
915
916        let mut theme = Config::builder().add_source(config::File::from_str(
917            &default_theme,
918            config::FileFormat::Toml,
919        ));
920
921        let theme_names = all_theme_names();
922
923        let override_theme = theme_name
924            .as_ref()
925            .and_then(|name| BUILTIN_THEMES.get(name.as_str()).copied())
926            .unwrap_or("");
927
928        theme = theme.add_source(config::File::from_str(
929            override_theme,
930            config::FileFormat::Toml,
931        ));
932        (theme, theme_names)
933    }
934
935    #[cfg(target_arch = "wasm32")]
936    pub fn new(theme_name: Option<String>) -> Result<Self> {
937        use eyre::anyhow;
938
939        let (theme, _) = Self::generate_defaults(theme_name.as_ref());
940
941        let theme = theme.set_override("theme_names", all_theme_names())?;
942
943        theme
944            .build()?
945            .try_deserialize()
946            .map_err(|e| anyhow!("Failed to parse config {e}"))
947    }
948
949    #[cfg(not(target_arch = "wasm32"))]
950    pub fn new(theme_name: Option<String>) -> eyre::Result<Self> {
951        use std::fs::ReadDir;
952
953        use eyre::anyhow;
954
955        let (mut theme, mut theme_names) = Self::generate_defaults(theme_name.as_ref());
956
957        let mut add_themes_from_dir = |dir: ReadDir| {
958            for theme in dir.flatten() {
959                if let Ok(theme_path) = theme.file_name().into_string()
960                    && let Some(fname_str) = theme_path.strip_suffix(".toml")
961                {
962                    let fname = fname_str.to_string();
963                    if !fname.is_empty() && !theme_names.contains(&fname) {
964                        theme_names.push(fname);
965                    }
966                }
967            }
968        };
969
970        // read themes from config directory
971        if let Some(proj_dirs) = &*PROJECT_DIR {
972            let config_themes_dir = proj_dirs.config_dir().join(THEMES_DIR);
973            if let Ok(config_themes_dir) = std::fs::read_dir(config_themes_dir) {
974                add_themes_from_dir(config_themes_dir);
975            }
976        }
977
978        // Read themes from local directories.
979        let local_config_dirs = find_local_configs();
980
981        // Add any existing themes from most top-level to most local. This allows overwriting of
982        // higher-level theme settings with a local `.surfer` directory.
983        local_config_dirs
984            .iter()
985            .filter_map(|p| std::fs::read_dir(p.join(THEMES_DIR)).ok())
986            .for_each(add_themes_from_dir);
987
988        if matches!(theme_name, Some(ref name) if !name.is_empty()) {
989            let theme_path =
990                Path::new(THEMES_DIR).join(theme_name.as_ref().unwrap().to_owned() + ".toml");
991
992            // First filter out all the existing local themes and add them in the aforementioned
993            // order.
994            let local_themes: Vec<PathBuf> = local_config_dirs
995                .iter()
996                .map(|p| p.join(&theme_path))
997                .filter(|p| p.exists())
998                .collect();
999            if local_themes.is_empty() {
1000                // If no local themes exist, search in the config directory.
1001                if let Some(proj_dirs) = &*PROJECT_DIR {
1002                    let config_theme_path = proj_dirs.config_dir().join(theme_path);
1003                    if config_theme_path.exists() {
1004                        theme = theme.add_source(File::from(config_theme_path).required(false));
1005                    }
1006                }
1007            } else {
1008                theme = local_themes
1009                    .into_iter()
1010                    .fold(theme, |t, p| t.add_source(File::from(p).required(false)));
1011            }
1012        }
1013
1014        let theme = theme.set_override("theme_names", theme_names)?;
1015
1016        theme
1017            .build()?
1018            .try_deserialize()
1019            .map_err(|e| anyhow!("Failed to parse theme {e}"))
1020    }
1021}
1022
1023#[derive(Debug, Deserialize)]
1024pub struct ThemeColorPair {
1025    #[serde(deserialize_with = "deserialize_hex_color")]
1026    pub foreground: Color32,
1027    #[serde(deserialize_with = "deserialize_hex_color")]
1028    pub background: Color32,
1029}
1030
1031#[derive(Debug, Deserialize)]
1032pub struct ThemeColorTriple {
1033    #[serde(deserialize_with = "deserialize_hex_color")]
1034    pub foreground: Color32,
1035    #[serde(deserialize_with = "deserialize_hex_color")]
1036    pub background: Color32,
1037    #[serde(deserialize_with = "deserialize_hex_color")]
1038    pub alt_background: Color32,
1039}
1040
1041#[derive(Debug, Deserialize)]
1042pub struct WcpConfig {
1043    /// Controls if a server is started after Surfer is launched
1044    pub autostart: bool,
1045    /// Address to bind to (address:port)
1046    pub address: String,
1047}
1048
1049fn default_colors() -> HashMap<String, Color32> {
1050    [
1051        ("Green", "a7e47e"),
1052        ("Red", "c52e2e"),
1053        ("Yellow", "f3d54a"),
1054        ("Blue", "81a2be"),
1055        ("Purple", "b294bb"),
1056        ("Aqua", "8abeb7"),
1057        ("Gray", "c5c8c6"),
1058    ]
1059    .iter()
1060    .map(|(name, hexcode)| {
1061        (
1062            (*name).to_string(),
1063            hex_string_to_color32((*hexcode).to_string()).unwrap(),
1064        )
1065    })
1066    .collect()
1067}
1068
1069impl SurferConfig {
1070    #[cfg(target_arch = "wasm32")]
1071    pub fn new(_force_default_config: bool) -> Result<Self> {
1072        Self::new_from_toml(&include_str!("../../default_config.toml"))
1073    }
1074
1075    #[cfg(not(target_arch = "wasm32"))]
1076    pub fn new(force_default_config: bool) -> eyre::Result<Self> {
1077        use eyre::anyhow;
1078        use tracing::warn;
1079
1080        let default_config = String::from(include_str!("../../default_config.toml"));
1081
1082        let mut config = Config::builder().add_source(config::File::from_str(
1083            &default_config,
1084            config::FileFormat::Toml,
1085        ));
1086
1087        let config = if force_default_config {
1088            config
1089        } else {
1090            if let Some(proj_dirs) = &*PROJECT_DIR {
1091                let config_file = proj_dirs.config_dir().join(CONFIG_FILE);
1092                config = config.add_source(File::from(config_file).required(false));
1093            }
1094
1095            let old_config_path = Path::new(OLD_CONFIG_FILE);
1096            if old_config_path.exists() {
1097                warn!(
1098                    "Configuration in 'surfer.toml' is deprecated. Please move your configuration to '.surfer/config.toml'."
1099                );
1100            }
1101
1102            // `surfer.toml` will not be searched for upward, as it is deprecated.
1103            config = config.add_source(File::from(old_config_path).required(false));
1104
1105            // Add configs from most top-level to most local. This allows overwriting of
1106            // higher-level settings with a local `.surfer` directory.
1107            find_local_configs()
1108                .into_iter()
1109                .fold(config, |c, p| {
1110                    c.add_source(File::from(p.join(CONFIG_FILE)).required(false))
1111                })
1112                .add_source(Environment::with_prefix("surfer")) // Add environment finally
1113        };
1114
1115        config
1116            .build()?
1117            .try_deserialize()
1118            .map_err(|e| anyhow!("Failed to parse config {e}"))
1119    }
1120
1121    pub fn new_from_toml(config: &str) -> Result<Self> {
1122        Ok(toml::from_str(config)?)
1123    }
1124}
1125
1126impl Default for SurferConfig {
1127    fn default() -> Self {
1128        Self::new(false).expect("Failed to load default config")
1129    }
1130}
1131
1132fn hex_string_to_color32(str: String) -> Result<Color32> {
1133    let str = if str.len() == 3 {
1134        str.chars().flat_map(|c| [c, c]).collect()
1135    } else {
1136        str
1137    };
1138    if str.len() == 6 {
1139        let r = u8::from_str_radix(&str[0..2], 16)
1140            .with_context(|| format!("'{str}' is not a valid RGB hex color"))?;
1141        let g = u8::from_str_radix(&str[2..4], 16)
1142            .with_context(|| format!("'{str}' is not a valid RGB hex color"))?;
1143        let b = u8::from_str_radix(&str[4..6], 16)
1144            .with_context(|| format!("'{str}' is not a valid RGB hex color"))?;
1145        Ok(Color32::from_rgb(r, g, b))
1146    } else {
1147        Result::Err(Report::msg(format!("'{str}' is not a valid RGB hex color")))
1148    }
1149}
1150
1151fn all_theme_names() -> Vec<String> {
1152    BUILTIN_THEMES.keys().map(ToString::to_string).collect()
1153}
1154
1155fn deserialize_hex_color<'de, D>(deserializer: D) -> Result<Color32, D::Error>
1156where
1157    D: Deserializer<'de>,
1158{
1159    let buf = String::deserialize(deserializer)?;
1160    hex_string_to_color32(buf).map_err(de::Error::custom)
1161}
1162
1163fn deserialize_color_map<'de, D>(deserializer: D) -> Result<HashMap<String, Color32>, D::Error>
1164where
1165    D: Deserializer<'de>,
1166{
1167    #[derive(Deserialize)]
1168    struct Wrapper(#[serde(deserialize_with = "deserialize_hex_color")] Color32);
1169
1170    let v = HashMap::<String, Wrapper>::deserialize(deserializer)?;
1171    Ok(v.into_iter().map(|(k, Wrapper(v))| (k, v)).collect())
1172}
1173
1174fn deserialize_theme<'de, D>(deserializer: D) -> Result<SurferTheme, D::Error>
1175where
1176    D: Deserializer<'de>,
1177{
1178    let buf = String::deserialize(deserializer)?;
1179    SurferTheme::new(Some(buf)).map_err(de::Error::custom)
1180}
1181
1182/// Searches for `.surfer` directories upward from the current location until it reaches root.
1183/// Returns an empty vector in case the search fails in any way. If any `.surfer` directories
1184/// are found, they will be returned in a `Vec<PathBuf>` in a pre-order of most top-level to most
1185/// local. All plain files are ignored.
1186#[cfg(not(target_arch = "wasm32"))]
1187pub fn find_local_configs() -> Vec<PathBuf> {
1188    use crate::util::search_upward;
1189    match std::env::current_dir() {
1190        Ok(dir) => search_upward(dir, "/", LOCAL_DIR)
1191            .into_iter()
1192            .filter(|p| p.is_dir()) // Only keep directories and ignore plain files.
1193            .rev() // Reverse for pre-order traversal of directories.
1194            .collect(),
1195        Err(_) => vec![],
1196    }
1197}
1198
1199#[cfg(test)]
1200mod tests {
1201    use super::*;
1202
1203    #[test]
1204    fn test_hex_string_3_chars() {
1205        // Test that 3-character hex strings are doubled correctly
1206        let result = hex_string_to_color32("abc".to_string()).unwrap();
1207        let expected = Color32::from_rgb(0xaa, 0xbb, 0xcc);
1208        assert_eq!(result, expected);
1209    }
1210
1211    #[test]
1212    fn test_hex_string_6_chars() {
1213        // Test standard 6-character hex string
1214        let result = hex_string_to_color32("a7e47e".to_string()).unwrap();
1215        let expected = Color32::from_rgb(0xa7, 0xe4, 0x7e);
1216        assert_eq!(result, expected);
1217    }
1218
1219    #[test]
1220    fn test_hex_string_black() {
1221        // Test black color (all zeros)
1222        let result = hex_string_to_color32("000000".to_string()).unwrap();
1223        let expected = Color32::from_rgb(0x00, 0x00, 0x00);
1224        assert_eq!(result, expected);
1225    }
1226
1227    #[test]
1228    fn test_hex_string_white() {
1229        // Test white color (all ones)
1230        let result = hex_string_to_color32("ffffff".to_string()).unwrap();
1231        let expected = Color32::from_rgb(0xff, 0xff, 0xff);
1232        assert_eq!(result, expected);
1233    }
1234
1235    #[test]
1236    fn test_hex_string_uppercase() {
1237        // Test uppercase hex characters
1238        let result = hex_string_to_color32("ABCDEF".to_string()).unwrap();
1239        let expected = Color32::from_rgb(0xab, 0xcd, 0xef);
1240        assert_eq!(result, expected);
1241    }
1242
1243    #[test]
1244    fn test_hex_string_mixed_case() {
1245        // Test mixed case hex characters
1246        let result = hex_string_to_color32("Ab5DeF".to_string()).unwrap();
1247        let expected = Color32::from_rgb(0xab, 0x5d, 0xef);
1248        assert_eq!(result, expected);
1249    }
1250
1251    #[test]
1252    fn test_hex_string_invalid_length() {
1253        // Test that invalid length returns error
1254        let result = hex_string_to_color32("ab".to_string());
1255        assert!(result.is_err());
1256
1257        let result = hex_string_to_color32("abcde".to_string());
1258        assert!(result.is_err());
1259
1260        let result = hex_string_to_color32("abcdefgh".to_string());
1261        assert!(result.is_err());
1262    }
1263
1264    #[test]
1265    fn test_hex_string_invalid_characters() {
1266        // Test that invalid hex characters return error
1267        let result = hex_string_to_color32("GGGGGG".to_string());
1268        assert!(result.is_err());
1269
1270        let result = hex_string_to_color32("12345g".to_string());
1271        assert!(result.is_err());
1272
1273        let result = hex_string_to_color32("zzzzzz".to_string());
1274        assert!(result.is_err());
1275    }
1276
1277    #[test]
1278    fn test_hex_string_empty() {
1279        // Test empty string
1280        let result = hex_string_to_color32(String::new());
1281        assert!(result.is_err());
1282    }
1283
1284    #[test]
1285    fn test_hex_string_3_chars_doubling() {
1286        // Test specific 3-character doubling behavior
1287        let result = hex_string_to_color32("050".to_string()).unwrap();
1288        let expected = Color32::from_rgb(0x00, 0x55, 0x00);
1289        assert_eq!(result, expected);
1290    }
1291}