1#[cfg(not(target_arch = "wasm32"))]
2use camino::{Utf8Path, Utf8PathBuf};
3use config::builder::DefaultState;
4use config::{Config, ConfigBuilder};
5#[cfg(not(target_arch = "wasm32"))]
6use config::{Environment, File};
7use derive_more::{Display, FromStr};
8#[cfg(not(target_arch = "wasm32"))]
9use directories::ProjectDirs;
10use ecolor::Color32;
11use enum_iterator::Sequence;
12use epaint::{PathStroke, Stroke};
13use eyre::{Report, Result, WrapErr as _, anyhow};
14use serde::de;
15use serde::{Deserialize, Deserializer, Serialize};
16use std::collections::HashMap;
17#[cfg(not(target_arch = "wasm32"))]
18use std::path::{Path, PathBuf};
19use std::sync::LazyLock;
20use surver::SurverConfig;
21use tracing::info;
22
23use crate::hierarchy::{HierarchyStyle, ParameterDisplayLocation};
24use crate::keyboard_shortcuts::{SurferShortcuts, deserialize_shortcuts};
25use crate::mousegestures::GestureZones;
26use crate::time::TimeFormat;
27use crate::trace_style::TraceStyle;
28use crate::wave_container::VariableMeta;
29use crate::{clock_highlighting::ClockHighlightType, variable_name_type::VariableNameType};
30use surfer_translation_types::{VariableEncoding, VariableType};
31
32macro_rules! theme {
33 ($name:expr) => {
34 (
35 $name,
36 include_str!(concat!("../../themes/", $name, ".toml")),
37 )
38 };
39}
40
41macro_rules! named_theme {
42 ($name:expr, $file:expr) => {
43 (
44 $name,
45 include_str!(concat!("../../themes/", $file, ".toml")),
46 )
47 };
48}
49
50static BUILTIN_THEMES: LazyLock<HashMap<&'static str, &'static str>> = LazyLock::new(|| {
52 HashMap::from([
53 theme!("dark+"),
54 theme!("dark-high-contrast"),
55 named_theme!("IBM", "ibm"),
56 theme!("light+"),
57 theme!("light-high-contrast"),
58 named_theme!("Okabe/Ito", "okabe-ito"),
59 named_theme!("Petroff Dark", "petroff-dark"),
60 named_theme!("Petroff Light", "petroff-light"),
61 named_theme!("Rosé Pine", "rose-pine"),
62 named_theme!("Rosé Pine Moon", "rose-pine-moon"),
63 named_theme!("Rosé Pine Dawn", "rose-pine-dawn"),
64 named_theme!("Solarized", "solarized"),
65 ])
66});
67
68#[cfg(not(target_arch = "wasm32"))]
69pub static PROJECT_DIR: LazyLock<Option<ProjectDirs>> =
70 LazyLock::new(|| ProjectDirs::from("org", "surfer-project", "surfer"));
71#[cfg(not(target_arch = "wasm32"))]
72const OLD_CONFIG_FILE: &str = "surfer.toml";
73#[cfg(not(target_arch = "wasm32"))]
74const CONFIG_FILE: &str = "config.toml";
75#[cfg(not(target_arch = "wasm32"))]
76const THEMES_DIR: &str = "themes";
77#[cfg(not(target_arch = "wasm32"))]
78pub const LOCAL_DIR: &str = ".surfer";
79
80#[derive(Clone, Copy, Debug, Deserialize, Display, FromStr, PartialEq, Eq, Sequence, Serialize)]
82pub enum ArrowKeyBindings {
83 Edge,
85
86 Scroll,
88}
89
90#[derive(Clone, Copy, Debug, Deserialize, Display, FromStr, PartialEq, Eq, Sequence, Serialize)]
91pub enum TransitionValue {
92 Previous,
94 Next,
96 Both,
98}
99
100#[derive(Debug, Deserialize, Display, PartialEq, Eq, Sequence, Serialize, Clone, Copy)]
102pub enum PrimaryMouseDrag {
103 #[display("Measure time")]
105 Measure,
106
107 #[display("Move cursor")]
109 Cursor,
110}
111
112#[derive(Debug, Deserialize, Display, PartialEq, Eq, Sequence, Serialize, Clone, Copy)]
113pub enum AutoLoad {
114 Always,
115 Never,
116 Ask,
117}
118
119impl AutoLoad {
120 #[must_use]
121 pub fn from_bool(auto_load: bool) -> Self {
122 if auto_load {
123 AutoLoad::Always
124 } else {
125 AutoLoad::Never
126 }
127 }
128}
129
130#[derive(Clone, Copy, Debug, Deserialize, Display, FromStr, PartialEq, Eq, Sequence, Serialize)]
131pub enum FocusHighlight {
132 Off,
133 Background,
134 LineWidth,
135 BrightnessShift,
136 LineWidthAndBrightnessShift,
137}
138
139#[derive(Debug, Deserialize)]
140pub struct SurferConfig {
141 pub layout: SurferLayout,
142 #[serde(deserialize_with = "deserialize_theme")]
143 pub theme: SurferTheme,
144 pub gesture: SurferGesture,
148 pub behavior: SurferBehavior,
149 pub default_time_format: TimeFormat,
151 pub default_variable_name_type: VariableNameType,
152 default_clock_highlight_type: ClockHighlightType,
153 #[serde(deserialize_with = "deserialize_non_negative_f32")]
155 pub snap_distance: f32,
156 pub undo_stack_size: usize,
158 autoreload_files: AutoLoad,
160 autoload_sibling_state_files: AutoLoad,
162 pub plugin: PluginConfig,
164 pub wcp: WcpConfig,
166 pub server: SurverConfig,
168 #[serde(deserialize_with = "deserialize_non_negative_f32")]
170 pub animation_time: f32,
171 pub animation_enabled: bool,
173 pub max_url_length: u16,
176 #[serde(deserialize_with = "deserialize_shortcuts")]
178 pub shortcuts: SurferShortcuts,
179 pub show_divider_text: bool,
181}
182
183impl SurferConfig {
184 #[must_use]
185 pub fn default_clock_highlight_type(&self) -> ClockHighlightType {
186 self.default_clock_highlight_type
187 }
188
189 #[must_use]
190 pub fn autoload_sibling_state_files(&self) -> AutoLoad {
191 self.autoload_sibling_state_files
192 }
193
194 #[must_use]
195 pub fn autoreload_files(&self) -> AutoLoad {
196 self.autoreload_files
197 }
198
199 #[must_use]
200 pub fn animation_enabled(&self) -> bool {
201 self.animation_enabled
202 }
203
204 #[must_use]
205 pub fn show_divider_text(&self) -> bool {
206 self.show_divider_text
207 }
208}
209
210#[derive(Debug, Deserialize)]
211pub struct SurferLayout {
212 show_hierarchy: bool,
214 show_menu: bool,
216 show_toolbar: bool,
218 show_ticks: bool,
220 show_tooltip: bool,
222 show_scope_tooltip: bool,
224 show_overview: bool,
226 show_statusbar: bool,
228 show_variable_indices: bool,
230 show_variable_direction: bool,
232 show_default_timeline: bool,
234 show_empty_scopes: bool,
236 show_hierarchy_icons: bool,
238 parameter_display_location: ParameterDisplayLocation,
240 pub window_height: usize,
242 pub window_width: usize,
244 pub window_x_position: usize,
246 pub window_y_position: usize,
248 align_names_right: bool,
250 hierarchy_style: HierarchyStyle,
252 #[serde(deserialize_with = "deserialize_non_negative_f32")]
254 pub waveforms_text_size: f32,
255 #[serde(deserialize_with = "deserialize_non_negative_f32")]
257 pub waveforms_line_height: f32,
258 #[serde(deserialize_with = "deserialize_non_negative_f32")]
260 pub waveforms_gap: f32,
261 #[serde(deserialize_with = "deserialize_non_negative_f32_vec")]
263 pub waveforms_line_height_multiples: Vec<f32>,
264 #[serde(deserialize_with = "deserialize_non_negative_f32")]
266 pub analog_waveform_multiplier: f32,
267 #[serde(deserialize_with = "deserialize_non_negative_f32")]
269 pub transactions_line_height: f32,
270 #[serde(deserialize_with = "deserialize_non_negative_f32_vec")]
272 pub zoom_factors: Vec<f32>,
273 #[serde(deserialize_with = "deserialize_non_negative_f32")]
275 default_zoom_factor: f32,
276 focus_highlight: FocusHighlight,
278 move_focus_on_inserted_marker: bool,
280 fill_high_values: bool,
282 draw_vector_unknowns_as_line: bool,
284 trace_style: TraceStyle,
286 transition_value: TransitionValue,
288 #[serde(default)]
290 toolbar: ToolbarLayout,
291 #[serde(default)]
294 pub enable_time_offset: bool,
295}
296
297#[derive(Debug, Deserialize, Default)]
298struct ToolbarLayout {
299 #[serde(default)]
301 row: HashMap<String, u8>,
302 #[serde(default)]
304 visibility: HashMap<String, bool>,
305}
306
307impl SurferLayout {
308 #[must_use]
309 pub fn show_hierarchy(&self) -> bool {
310 self.show_hierarchy
311 }
312 #[must_use]
313 pub fn show_menu(&self) -> bool {
314 self.show_menu
315 }
316 #[must_use]
317 pub fn show_ticks(&self) -> bool {
318 self.show_ticks
319 }
320 #[must_use]
321 pub fn show_tooltip(&self) -> bool {
322 self.show_tooltip
323 }
324 #[must_use]
325 pub fn show_scope_tooltip(&self) -> bool {
326 self.show_scope_tooltip
327 }
328 #[must_use]
329 pub fn show_default_timeline(&self) -> bool {
330 self.show_default_timeline
331 }
332 #[must_use]
333 pub fn show_toolbar(&self) -> bool {
334 self.show_toolbar
335 }
336 #[must_use]
337 pub fn show_overview(&self) -> bool {
338 self.show_overview
339 }
340 #[must_use]
341 pub fn show_statusbar(&self) -> bool {
342 self.show_statusbar
343 }
344 #[must_use]
345 pub fn align_names_right(&self) -> bool {
346 self.align_names_right
347 }
348 #[must_use]
349 pub fn show_variable_indices(&self) -> bool {
350 self.show_variable_indices
351 }
352 #[must_use]
353 pub fn show_variable_direction(&self) -> bool {
354 self.show_variable_direction
355 }
356 #[must_use]
357 pub fn default_zoom_factor(&self) -> f32 {
358 self.default_zoom_factor
359 }
360 #[must_use]
361 pub fn show_empty_scopes(&self) -> bool {
362 self.show_empty_scopes
363 }
364 #[must_use]
365 pub fn show_hierarchy_icons(&self) -> bool {
366 self.show_hierarchy_icons
367 }
368 #[must_use]
369 pub fn parameter_display_location(&self) -> ParameterDisplayLocation {
370 self.parameter_display_location
371 }
372 #[must_use]
373 pub fn focus_highlight(&self) -> FocusHighlight {
374 self.focus_highlight
375 }
376 #[must_use]
377 pub fn move_focus_on_inserted_marker(&self) -> bool {
378 self.move_focus_on_inserted_marker
379 }
380 #[must_use]
381 pub fn fill_high_values(&self) -> bool {
382 self.fill_high_values
383 }
384 #[must_use]
385 pub fn draw_vector_unknowns_as_line(&self) -> bool {
386 self.draw_vector_unknowns_as_line
387 }
388 #[must_use]
389 pub fn hierarchy_style(&self) -> HierarchyStyle {
390 self.hierarchy_style
391 }
392 #[must_use]
393 pub fn trace_style(&self) -> TraceStyle {
394 self.trace_style
395 }
396 #[must_use]
397 pub fn transition_value(&self) -> TransitionValue {
398 self.transition_value
399 }
400 #[must_use]
401 pub fn toolbar_group_row(&self, id: &str) -> Option<u8> {
402 self.toolbar.row.get(id).copied()
403 }
404 #[must_use]
405 pub fn toolbar_group_visibility(&self, id: &str) -> Option<bool> {
406 self.toolbar.visibility.get(id).copied()
407 }
408 #[must_use]
409 pub fn enable_time_offset(&self) -> bool {
410 self.enable_time_offset
411 }
412}
413
414#[derive(Debug, Deserialize)]
415pub struct SurferBehavior {
416 pub keep_during_reload: bool,
418 pub file_history_size: usize,
420 arrow_key_bindings: ArrowKeyBindings,
422 primary_button_drag_behavior: PrimaryMouseDrag,
425}
426
427impl SurferBehavior {
428 #[must_use]
429 pub fn file_history_size(&self) -> usize {
430 self.file_history_size
431 }
432
433 #[must_use]
434 pub fn primary_button_drag_behavior(&self) -> PrimaryMouseDrag {
435 self.primary_button_drag_behavior
436 }
437
438 #[must_use]
439 pub fn arrow_key_bindings(&self) -> ArrowKeyBindings {
440 self.arrow_key_bindings
441 }
442}
443
444#[derive(Debug, Deserialize)]
445pub struct SurferGesture {
449 #[serde(deserialize_with = "deserialize_non_negative_f32")]
451 pub size: f32,
452 #[serde(deserialize_with = "deserialize_non_negative_f32")]
454 pub deadzone: f32,
455 #[serde(deserialize_with = "deserialize_non_negative_f32")]
457 pub background_radius: f32,
458 #[serde(deserialize_with = "deserialize_unit_interval_f32")]
460 pub background_gamma: f32,
461 pub mapping: GestureZones,
463}
464
465#[derive(Clone, Debug, Deserialize)]
466pub struct SurferLineStyle {
467 #[serde(deserialize_with = "deserialize_hex_color")]
468 pub color: Color32,
469 #[serde(deserialize_with = "deserialize_non_negative_f32")]
470 pub width: f32,
471}
472
473impl From<SurferLineStyle> for Stroke {
474 fn from(style: SurferLineStyle) -> Self {
475 Stroke {
476 color: style.color,
477 width: style.width,
478 }
479 }
480}
481
482impl From<&SurferLineStyle> for Stroke {
483 fn from(style: &SurferLineStyle) -> Self {
484 Stroke {
485 color: style.color,
486 width: style.width,
487 }
488 }
489}
490
491impl From<&SurferLineStyle> for PathStroke {
492 fn from(style: &SurferLineStyle) -> Self {
493 PathStroke::new(style.width, style.color)
494 }
495}
496
497#[derive(Debug, Deserialize)]
498pub struct SurferTicks {
500 #[serde(deserialize_with = "deserialize_unit_interval_f32")]
502 pub density: f32,
503 pub style: SurferLineStyle,
505}
506
507#[derive(Debug, Deserialize)]
508pub struct SurferRelationArrow {
509 pub style: SurferLineStyle,
511
512 #[serde(deserialize_with = "deserialize_non_negative_f32")]
514 pub head_angle: f32,
515
516 #[serde(deserialize_with = "deserialize_non_negative_f32")]
518 pub head_length: f32,
519}
520
521#[derive(Debug, Deserialize)]
522pub struct SurferTheme {
523 #[serde(deserialize_with = "deserialize_hex_color")]
525 pub foreground: Color32,
526 #[serde(deserialize_with = "deserialize_hex_color")]
527 pub border_color: Color32,
529 #[serde(deserialize_with = "deserialize_hex_color")]
531 pub alt_text_color: Color32,
532 pub canvas_colors: ThemeColorTriple,
534 pub primary_ui_color: ThemeColorPair,
536 pub secondary_ui_color: ThemeColorPair,
539 pub selected_elements_colors: ThemeColorPair,
541
542 pub accent_info: ThemeColorPair,
543 pub accent_warn: ThemeColorPair,
544 pub accent_error: ThemeColorPair,
545
546 pub cursor: SurferLineStyle,
548
549 pub gesture: SurferLineStyle,
551
552 pub measure: SurferLineStyle,
554
555 pub annotation_rectangle: SurferLineStyle,
557
558 pub annotation_arrow: SurferLineStyle,
560
561 pub clock_highlight_line: SurferLineStyle,
563 #[serde(deserialize_with = "deserialize_hex_color_vec")]
564 pub clock_highlight_line_colors: Vec<Color32>,
566 #[serde(deserialize_with = "deserialize_hex_color")]
567 pub clock_highlight_cycle: Color32,
568 #[serde(deserialize_with = "deserialize_hex_color_vec")]
569 pub clock_highlight_cycle_colors: Vec<Color32>,
571 pub clock_rising_marker: bool,
573
574 #[serde(deserialize_with = "deserialize_hex_color")]
575 pub variable_default: Color32,
577 #[serde(deserialize_with = "deserialize_hex_color")]
578 pub variable_highimp: Color32,
580 #[serde(deserialize_with = "deserialize_hex_color")]
581 pub variable_undef: Color32,
583 #[serde(deserialize_with = "deserialize_hex_color")]
584 pub variable_dontcare: Color32,
586 #[serde(deserialize_with = "deserialize_hex_color")]
587 pub variable_weak: Color32,
589 #[serde(deserialize_with = "deserialize_hex_color")]
590 pub variable_parameter: Color32,
592 #[serde(deserialize_with = "deserialize_hex_color")]
593 pub transaction_default: Color32,
595 pub relation_arrow: SurferRelationArrow,
597 #[serde(deserialize_with = "deserialize_hex_color")]
598 pub variable_event: Color32,
600
601 #[serde(deserialize_with = "deserialize_unit_interval_f32")]
604 pub waveform_opacity: f32,
605 #[serde(deserialize_with = "deserialize_unit_interval_f32")]
607 pub wide_opacity: f32,
608
609 #[serde(deserialize_with = "deserialize_color_map")]
610 pub colors: HashMap<String, Color32>,
611 #[serde(deserialize_with = "deserialize_hex_color")]
612 pub highlight_background: Color32,
613
614 #[serde(deserialize_with = "deserialize_non_negative_f32")]
616 pub linewidth: f32,
617
618 #[serde(deserialize_with = "deserialize_non_negative_f32")]
620 pub thick_linewidth: f32,
621 #[serde(deserialize_with = "deserialize_non_negative_f32")]
623 pub focus_highlight_line_width_multiplier: f32,
624 #[serde(deserialize_with = "deserialize_unit_interval_f32")]
626 pub focus_highlight_brightness_shift: f32,
627
628 #[serde(deserialize_with = "deserialize_non_negative_f32")]
630 pub vector_transition_width: f32,
631
632 pub alt_frequency: usize,
635
636 pub viewport_separator: SurferLineStyle,
638
639 #[serde(deserialize_with = "deserialize_hex_color")]
641 pub drag_hint_color: Color32,
642 #[serde(deserialize_with = "deserialize_non_negative_f32")]
643 pub drag_hint_width: f32,
644 #[serde(deserialize_with = "deserialize_non_negative_f32")]
645 pub drag_threshold: f32,
646
647 pub ticks: SurferTicks,
649
650 pub theme_names: Vec<String>,
652
653 #[serde(default)]
655 pub theme_name: String,
656
657 #[serde(default)]
659 pub scope_icons: ScopeIcons,
660
661 #[serde(default)]
663 pub variable_icons: VariableIcons,
664}
665
666#[derive(Clone, Debug, Deserialize)]
668#[serde(default)]
669pub struct ScopeIconColors {
670 #[serde(deserialize_with = "deserialize_hex_color")]
671 pub module: Color32,
672 #[serde(deserialize_with = "deserialize_hex_color")]
673 pub task: Color32,
674 #[serde(deserialize_with = "deserialize_hex_color")]
675 pub function: Color32,
676 #[serde(deserialize_with = "deserialize_hex_color")]
677 pub begin: Color32,
678 #[serde(deserialize_with = "deserialize_hex_color")]
679 pub fork: Color32,
680 #[serde(deserialize_with = "deserialize_hex_color")]
681 pub generate: Color32,
682 #[serde(rename = "struct", deserialize_with = "deserialize_hex_color")]
683 pub struct_: Color32,
684 #[serde(deserialize_with = "deserialize_hex_color")]
685 pub union: Color32,
686 #[serde(deserialize_with = "deserialize_hex_color")]
687 pub class: Color32,
688 #[serde(deserialize_with = "deserialize_hex_color")]
689 pub interface: Color32,
690 #[serde(deserialize_with = "deserialize_hex_color")]
691 pub package: Color32,
692 #[serde(deserialize_with = "deserialize_hex_color")]
693 pub program: Color32,
694 #[serde(deserialize_with = "deserialize_hex_color")]
695 pub vhdl_architecture: Color32,
696 #[serde(deserialize_with = "deserialize_hex_color")]
697 pub vhdl_procedure: Color32,
698 #[serde(deserialize_with = "deserialize_hex_color")]
699 pub vhdl_function: Color32,
700 #[serde(deserialize_with = "deserialize_hex_color")]
701 pub vhdl_record: Color32,
702 #[serde(deserialize_with = "deserialize_hex_color")]
703 pub vhdl_process: Color32,
704 #[serde(deserialize_with = "deserialize_hex_color")]
705 pub vhdl_block: Color32,
706 #[serde(deserialize_with = "deserialize_hex_color")]
707 pub vhdl_for_generate: Color32,
708 #[serde(deserialize_with = "deserialize_hex_color")]
709 pub vhdl_if_generate: Color32,
710 #[serde(deserialize_with = "deserialize_hex_color")]
711 pub vhdl_generate: Color32,
712 #[serde(deserialize_with = "deserialize_hex_color")]
713 pub vhdl_package: Color32,
714 #[serde(deserialize_with = "deserialize_hex_color")]
715 pub ghw_generic: Color32,
716 #[serde(deserialize_with = "deserialize_hex_color")]
717 pub vhdl_array: Color32,
718 #[serde(deserialize_with = "deserialize_hex_color")]
719 pub unknown: Color32,
720 #[serde(deserialize_with = "deserialize_hex_color")]
721 pub clocking: Color32,
722 #[serde(deserialize_with = "deserialize_hex_color")]
723 pub sv_array: Color32,
724}
725
726impl Default for ScopeIconColors {
727 fn default() -> Self {
728 Self {
729 module: Color32::from_rgb(0x4F, 0xC3, 0xF7), task: Color32::from_rgb(0xFF, 0xB7, 0x4D), function: Color32::from_rgb(0xBA, 0x68, 0xC8), begin: Color32::from_rgb(0x81, 0xC7, 0x84), fork: Color32::from_rgb(0xFF, 0x80, 0x80), generate: Color32::from_rgb(0x64, 0xB5, 0xF6), struct_: Color32::from_rgb(0x4D, 0xD0, 0xE1), union: Color32::from_rgb(0x4D, 0xD0, 0xE1), class: Color32::from_rgb(0xF0, 0x62, 0x92), interface: Color32::from_rgb(0xAE, 0xD5, 0x81), package: Color32::from_rgb(0xFF, 0xD5, 0x4F), program: Color32::from_rgb(0xA1, 0x88, 0x7F), vhdl_architecture: Color32::from_rgb(0x4F, 0xC3, 0xF7), vhdl_procedure: Color32::from_rgb(0xFF, 0xB7, 0x4D), vhdl_function: Color32::from_rgb(0xBA, 0x68, 0xC8), vhdl_record: Color32::from_rgb(0x4D, 0xD0, 0xE1), vhdl_process: Color32::from_rgb(0x81, 0xC7, 0x84), vhdl_block: Color32::from_rgb(0x90, 0xA4, 0xAE), vhdl_for_generate: Color32::from_rgb(0x64, 0xB5, 0xF6), vhdl_if_generate: Color32::from_rgb(0x64, 0xB5, 0xF6), vhdl_generate: Color32::from_rgb(0x64, 0xB5, 0xF6), vhdl_package: Color32::from_rgb(0xFF, 0xD5, 0x4F), ghw_generic: Color32::from_rgb(0xB0, 0xBE, 0xC5), vhdl_array: Color32::from_rgb(0xCE, 0x93, 0xD8), clocking: Color32::from_rgb(0xF0, 0x62, 0x92), sv_array: Color32::from_rgb(0xCE, 0x93, 0xD8), unknown: Color32::from_rgb(0x9E, 0x9E, 0x9E), }
757 }
758}
759
760#[derive(Clone, Debug, Deserialize)]
763#[serde(default)]
764pub struct ScopeIcons {
765 pub module: String,
767 pub task: String,
768 pub function: String,
769 pub begin: String,
770 pub fork: String,
771 pub generate: String,
772 #[serde(rename = "struct")]
773 pub struct_: String,
774 pub union: String,
775 pub class: String,
776 pub interface: String,
777 pub package: String,
778 pub program: String,
779 pub vhdl_architecture: String,
781 pub vhdl_procedure: String,
782 pub vhdl_function: String,
783 pub vhdl_record: String,
784 pub vhdl_process: String,
785 pub vhdl_block: String,
786 pub vhdl_for_generate: String,
787 pub vhdl_if_generate: String,
788 pub vhdl_generate: String,
789 pub vhdl_package: String,
790 pub ghw_generic: String,
791 pub vhdl_array: String,
792 pub unknown: String,
793 pub clocking: String,
794 pub sv_array: String,
795 #[serde(default)]
797 pub colors: ScopeIconColors,
798}
799
800impl Default for ScopeIcons {
801 fn default() -> Self {
802 use egui_remixicon::icons;
803 Self {
804 module: icons::CPU_LINE.to_string(),
806 task: icons::TASK_LINE.to_string(),
807 function: icons::BRACES_LINE.to_string(),
808 begin: icons::CODE_BOX_LINE.to_string(),
809 fork: icons::GIT_BRANCH_LINE.to_string(),
810 generate: icons::REPEAT_LINE.to_string(),
811 struct_: icons::TABLE_LINE.to_string(),
812 union: icons::MERGE_CELLS_HORIZONTAL.to_string(),
813 class: icons::TABLE_LINE.to_string(),
814 interface: icons::PLUG_LINE.to_string(),
815 package: icons::BOX_3_LINE.to_string(),
816 program: icons::FILE_CODE_LINE.to_string(),
817 vhdl_architecture: icons::CPU_LINE.to_string(),
819 vhdl_procedure: icons::TERMINAL_LINE.to_string(),
820 vhdl_function: icons::BRACES_LINE.to_string(),
821 vhdl_record: icons::TABLE_LINE.to_string(),
822 vhdl_process: icons::FLASHLIGHT_LINE.to_string(),
823 vhdl_block: icons::CODE_BLOCK.to_string(),
824 vhdl_for_generate: icons::REPEAT_LINE.to_string(),
825 vhdl_if_generate: icons::QUESTION_LINE.to_string(),
826 vhdl_generate: icons::REPEAT_LINE.to_string(),
827 vhdl_package: icons::BOX_3_LINE.to_string(),
828 ghw_generic: icons::SETTINGS_3_LINE.to_string(),
829 vhdl_array: icons::BRACKETS_LINE.to_string(),
830 sv_array: icons::BRACKETS_LINE.to_string(),
831 clocking: icons::TIME_LINE.to_string(),
832 unknown: icons::QUESTION_LINE.to_string(),
833 colors: ScopeIconColors::default(),
834 }
835 }
836}
837
838impl ScopeIcons {
839 #[must_use]
842 pub fn get_icon(&self, scope_type: Option<wellen::ScopeType>) -> (&str, Color32) {
843 use wellen::ScopeType;
844 match scope_type {
845 None => (&self.module, self.colors.module),
846 Some(st) => match st {
847 ScopeType::Module => (&self.module, self.colors.module),
848 ScopeType::Task => (&self.task, self.colors.task),
849 ScopeType::Function => (&self.function, self.colors.function),
850 ScopeType::Begin => (&self.begin, self.colors.begin),
851 ScopeType::Fork => (&self.fork, self.colors.fork),
852 ScopeType::Generate => (&self.generate, self.colors.generate),
853 ScopeType::Struct => (&self.struct_, self.colors.struct_),
854 ScopeType::Union => (&self.union, self.colors.union),
855 ScopeType::Class => (&self.class, self.colors.class),
856 ScopeType::Interface => (&self.interface, self.colors.interface),
857 ScopeType::Package => (&self.package, self.colors.package),
858 ScopeType::Program => (&self.program, self.colors.program),
859 ScopeType::VhdlArchitecture => {
860 (&self.vhdl_architecture, self.colors.vhdl_architecture)
861 }
862 ScopeType::VhdlProcedure => (&self.vhdl_procedure, self.colors.vhdl_procedure),
863 ScopeType::VhdlFunction => (&self.vhdl_function, self.colors.vhdl_function),
864 ScopeType::VhdlRecord => (&self.vhdl_record, self.colors.vhdl_record),
865 ScopeType::VhdlProcess => (&self.vhdl_process, self.colors.vhdl_process),
866 ScopeType::VhdlBlock => (&self.vhdl_block, self.colors.vhdl_block),
867 ScopeType::VhdlForGenerate => {
868 (&self.vhdl_for_generate, self.colors.vhdl_for_generate)
869 }
870 ScopeType::VhdlIfGenerate => (&self.vhdl_if_generate, self.colors.vhdl_if_generate),
871 ScopeType::VhdlGenerate => (&self.vhdl_generate, self.colors.vhdl_generate),
872 ScopeType::VhdlPackage => (&self.vhdl_package, self.colors.vhdl_package),
873 ScopeType::GhwGeneric => (&self.ghw_generic, self.colors.ghw_generic),
874 ScopeType::VhdlArray => (&self.vhdl_array, self.colors.vhdl_array),
875 ScopeType::Unknown => (&self.unknown, self.colors.unknown),
876 ScopeType::SvArray => (&self.sv_array, self.colors.sv_array),
877 ScopeType::Clocking => (&self.clocking, self.colors.clocking),
878 _ => (&self.unknown, self.colors.unknown),
879 },
880 }
881 }
882}
883
884#[derive(Clone, Debug, Deserialize)]
887#[serde(default)]
888pub struct VariableIconColors {
889 #[serde(deserialize_with = "deserialize_hex_color")]
891 pub wire: Color32,
892 #[serde(deserialize_with = "deserialize_hex_color")]
894 pub bus: Color32,
895 #[serde(deserialize_with = "deserialize_hex_color")]
897 pub string: Color32,
898 #[serde(deserialize_with = "deserialize_hex_color")]
900 pub event: Color32,
901 #[serde(deserialize_with = "deserialize_hex_color")]
903 pub other: Color32,
904}
905
906impl Default for VariableIconColors {
907 fn default() -> Self {
908 Self {
909 wire: Color32::from_rgb(0x81, 0xC7, 0x84), bus: Color32::from_rgb(0x64, 0xB5, 0xF6), string: Color32::from_rgb(0xFF, 0xB7, 0x4D), event: Color32::from_rgb(0xF0, 0x62, 0x92), other: Color32::from_rgb(0xBA, 0x68, 0xC8), }
915 }
916}
917
918#[derive(Clone, Debug, Deserialize)]
921#[serde(default)]
922pub struct VariableIcons {
923 pub wire: String,
925 pub bus: String,
927 pub string: String,
929 pub event: String,
931 pub other: String,
933 #[serde(default)]
935 pub colors: VariableIconColors,
936}
937
938impl Default for VariableIcons {
939 fn default() -> Self {
940 use egui_remixicon::icons;
941 Self {
942 wire: icons::GIT_COMMIT_LINE.to_string(),
943 bus: icons::BRACKETS_LINE.to_string(),
944 string: icons::TEXT.to_string(),
945 event: icons::ARROW_UP_LONG_LINE.to_string(),
946 other: icons::NUMBERS_LINE.to_string(),
947 colors: VariableIconColors::default(),
948 }
949 }
950}
951
952impl VariableIcons {
953 #[must_use]
956 pub fn get_icon(&self, meta: Option<&VariableMeta>) -> (&str, Color32) {
957 let Some(meta) = meta else {
958 return (&self.other, self.colors.other);
959 };
960
961 if matches!(
962 meta.variable_type,
963 Some(VariableType::VCDEvent | VariableType::EventParameter)
964 ) {
965 return (&self.event, self.colors.event);
966 }
967
968 match meta.encoding {
969 VariableEncoding::String => (&self.string, self.colors.string),
970 VariableEncoding::Real => (&self.other, self.colors.other),
971 VariableEncoding::BitVector => match meta.num_bits {
972 Some(1) => (&self.wire, self.colors.wire),
973 Some(n) if n > 1 => (&self.bus, self.colors.bus),
974 _ => (&self.other, self.colors.other),
975 },
976 }
977 }
978}
979
980fn gamma_correction(value: u8) -> f32 {
981 const INV_3294: f32 = 1.0 / 3294.0;
982 const INV_269: f32 = 1.0 / 269.0;
983
984 let v = f32::from(value);
985 if value < 10 {
986 v * INV_3294
987 } else {
988 (v * INV_269 + 0.0513).powf(2.4)
989 }
990}
991
992pub(crate) fn get_luminance(color: Color32) -> f32 {
993 0.2126 * gamma_correction(color.r())
994 + 0.7152 * gamma_correction(color.g())
995 + 0.0722 * gamma_correction(color.b())
996}
997
998impl SurferTheme {
999 #[must_use]
1000 pub fn get_color(&self, color: &str) -> Option<Color32> {
1001 self.colors.get(color).copied()
1002 }
1003
1004 #[must_use]
1005 pub fn get_best_text_color(&self, backgroundcolor: Color32) -> Color32 {
1006 let l_foreground = get_luminance(self.foreground);
1010 let l_alt_text_color = get_luminance(self.alt_text_color);
1011 let l_background = get_luminance(backgroundcolor);
1012
1013 let mut cr_foreground = (l_foreground + 0.05) / (l_background + 0.05);
1015 cr_foreground = cr_foreground.max(1. / cr_foreground);
1016 let mut cr_alt_text_color = (l_alt_text_color + 0.05) / (l_background + 0.05);
1017 cr_alt_text_color = cr_alt_text_color.max(1. / cr_alt_text_color);
1018
1019 if cr_foreground > cr_alt_text_color {
1021 self.foreground
1022 } else {
1023 self.alt_text_color
1024 }
1025 }
1026
1027 fn generate_defaults(
1028 theme_name: Option<&String>,
1029 ) -> (ConfigBuilder<DefaultState>, Vec<String>) {
1030 let default_theme = String::from(include_str!("../../default_theme.toml"));
1031
1032 let mut theme = Config::builder().add_source(config::File::from_str(
1033 &default_theme,
1034 config::FileFormat::Toml,
1035 ));
1036
1037 let theme_names = all_theme_names();
1038
1039 let override_theme = theme_name
1040 .as_ref()
1041 .and_then(|name| BUILTIN_THEMES.get(name.as_str()).copied())
1042 .unwrap_or("");
1043
1044 theme = theme.add_source(config::File::from_str(
1045 override_theme,
1046 config::FileFormat::Toml,
1047 ));
1048 (theme, theme_names)
1049 }
1050
1051 #[cfg(target_arch = "wasm32")]
1052 pub fn new(theme_name: Option<String>) -> Result<Self> {
1053 let theme_name = theme_name.filter(|s| !s.is_empty());
1054 let (theme, _) = Self::generate_defaults(theme_name.as_ref());
1055
1056 let theme = theme.set_override("theme_names", all_theme_names())?;
1057
1058 let mut result: SurferTheme = theme
1059 .build()?
1060 .try_deserialize()
1061 .map_err(|e| anyhow!("Failed to parse config {e}"))?;
1062 result.theme_name = theme_name.unwrap_or_default();
1063 Ok(result)
1064 }
1065
1066 #[cfg(not(target_arch = "wasm32"))]
1067 pub fn new(theme_name: Option<String>) -> Result<Self> {
1068 use std::fs::ReadDir;
1069
1070 let theme_name = theme_name.filter(|s| !s.is_empty());
1071 let (mut theme, mut theme_names) = Self::generate_defaults(theme_name.as_ref());
1072
1073 let mut add_themes_from_dir = |dir: ReadDir| {
1074 for theme in dir.flatten() {
1075 if let Ok(theme_path) = theme.file_name().into_string()
1076 && let Some(fname_str) = theme_path.strip_suffix(".toml")
1077 {
1078 let fname = fname_str.to_string();
1079 if !fname.is_empty() && !theme_names.contains(&fname) {
1080 theme_names.push(fname);
1081 }
1082 }
1083 }
1084 };
1085
1086 if let Some(proj_dirs) = &*PROJECT_DIR {
1088 let config_themes_dir = proj_dirs.config_dir().join(THEMES_DIR);
1089 if let Ok(config_themes_dir) = std::fs::read_dir(config_themes_dir) {
1090 add_themes_from_dir(config_themes_dir);
1091 }
1092 }
1093
1094 let local_config_dirs = find_local_configs();
1096
1097 local_config_dirs
1100 .iter()
1101 .filter_map(|p| std::fs::read_dir(p.join(THEMES_DIR)).ok())
1102 .for_each(add_themes_from_dir);
1103
1104 if let Some(name) = theme_name.as_ref()
1105 && !name.is_empty()
1106 {
1107 let theme_path = Utf8Path::new(THEMES_DIR).join(name.to_owned() + ".toml");
1108
1109 let local_themes: Vec<Utf8PathBuf> = local_config_dirs
1112 .iter()
1113 .map(|p| p.join(&theme_path))
1114 .filter(|p| p.exists())
1115 .collect();
1116 if local_themes.is_empty() {
1117 if let Some(proj_dirs) = &*PROJECT_DIR {
1119 let config_theme_path = proj_dirs.config_dir().join(theme_path);
1120 if config_theme_path.exists() {
1121 theme = theme.add_source(File::from(config_theme_path).required(false));
1122 }
1123 }
1124 } else {
1125 theme = local_themes.into_iter().fold(theme, |t, p| {
1126 t.add_source(File::from(p.into_std_path_buf()).required(false))
1127 });
1128 }
1129 }
1130
1131 let theme = theme.set_override("theme_names", theme_names)?;
1132
1133 let mut result: SurferTheme = theme
1134 .build()?
1135 .try_deserialize()
1136 .map_err(|e| anyhow!("Failed to parse theme {e}"))?;
1137 result.theme_name = theme_name.unwrap_or_default();
1138 Ok(result)
1139 }
1140}
1141
1142#[derive(Debug, Deserialize)]
1143pub struct ThemeColorPair {
1144 #[serde(deserialize_with = "deserialize_hex_color")]
1145 pub foreground: Color32,
1146 #[serde(deserialize_with = "deserialize_hex_color")]
1147 pub background: Color32,
1148}
1149
1150#[derive(Debug, Deserialize)]
1151pub struct ThemeColorTriple {
1152 #[serde(deserialize_with = "deserialize_hex_color")]
1153 pub foreground: Color32,
1154 #[serde(deserialize_with = "deserialize_hex_color")]
1155 pub background: Color32,
1156 #[serde(deserialize_with = "deserialize_hex_color")]
1157 pub alt_background: Color32,
1158}
1159
1160#[derive(Debug, Deserialize)]
1161pub struct PluginConfig {
1162 pub max_memory_mib: u64,
1164}
1165
1166#[derive(Debug, Deserialize)]
1167pub struct WcpConfig {
1168 pub autostart: bool,
1170 pub address: String,
1172}
1173
1174impl SurferConfig {
1175 #[cfg(target_arch = "wasm32")]
1176 pub fn new(_force_default_config: bool) -> Result<Self> {
1177 Self::new_from_toml(&include_str!("../../default_config.toml"))
1178 }
1179
1180 #[cfg(not(target_arch = "wasm32"))]
1181 pub fn new(force_default_config: bool) -> Result<Self> {
1182 use tracing::warn;
1183
1184 let default_config = String::from(include_str!("../../default_config.toml"));
1185
1186 let mut config = Config::builder().add_source(config::File::from_str(
1187 &default_config,
1188 config::FileFormat::Toml,
1189 ));
1190
1191 let config = if force_default_config {
1192 config
1193 } else {
1194 if let Some(proj_dirs) = &*PROJECT_DIR {
1195 let config_file = proj_dirs.config_dir().join(CONFIG_FILE);
1196 config = config.add_source(File::from(config_file).required(false));
1197 }
1198
1199 let old_config_path = Utf8Path::new(OLD_CONFIG_FILE);
1200 if old_config_path.exists() {
1201 warn!(
1202 "Configuration in 'surfer.toml' is deprecated. Please move your configuration to '.surfer/config.toml'."
1203 );
1204 }
1205
1206 config = config.add_source(File::from(old_config_path.as_std_path()).required(false));
1208
1209 find_local_configs()
1212 .into_iter()
1213 .fold(config, |c, p| {
1214 c.add_source(
1215 File::from(p.join(CONFIG_FILE).into_std_path_buf()).required(false),
1216 )
1217 })
1218 .add_source(Environment::with_prefix("surfer")) };
1220
1221 config
1222 .build()?
1223 .try_deserialize()
1224 .map_err(|e| anyhow!("Failed to parse config {e}"))
1225 }
1226
1227 pub fn new_from_toml(config: &str) -> Result<Self> {
1228 Ok(toml::from_str(config)?)
1229 }
1230}
1231
1232impl Default for SurferConfig {
1233 fn default() -> Self {
1234 Self::new(false).expect("Failed to load default config")
1235 }
1236}
1237
1238fn hex_string_to_color32(str: &str) -> Result<Color32> {
1239 let str = str.strip_prefix('#').unwrap_or(str).to_string();
1240 let str = if str.len() == 3 {
1241 str.chars().flat_map(|c| [c, c]).collect()
1242 } else {
1243 str
1244 };
1245 if str.len() == 6 {
1246 let r = u8::from_str_radix(&str[0..2], 16)
1247 .with_context(|| format!("'{str}' is not a valid RGB hex color"))?;
1248 let g = u8::from_str_radix(&str[2..4], 16)
1249 .with_context(|| format!("'{str}' is not a valid RGB hex color"))?;
1250 let b = u8::from_str_radix(&str[4..6], 16)
1251 .with_context(|| format!("'{str}' is not a valid RGB hex color"))?;
1252 Ok(Color32::from_rgb(r, g, b))
1253 } else {
1254 Result::Err(Report::msg(format!("'{str}' is not a valid RGB hex color")))
1255 }
1256}
1257
1258fn all_theme_names() -> Vec<String> {
1259 let mut names: Vec<String> = BUILTIN_THEMES.keys().map(ToString::to_string).collect();
1260 names.sort();
1261 names
1262}
1263
1264fn deserialize_hex_color<'de, D>(deserializer: D) -> Result<Color32, D::Error>
1265where
1266 D: Deserializer<'de>,
1267{
1268 let buf = String::deserialize(deserializer)?;
1269 hex_string_to_color32(&buf).map_err(de::Error::custom)
1270}
1271
1272fn deserialize_color_map<'de, D>(deserializer: D) -> Result<HashMap<String, Color32>, D::Error>
1273where
1274 D: Deserializer<'de>,
1275{
1276 #[derive(Deserialize)]
1277 struct Wrapper(#[serde(deserialize_with = "deserialize_hex_color")] Color32);
1278
1279 let v = HashMap::<String, Wrapper>::deserialize(deserializer)?;
1280 Ok(v.into_iter().map(|(k, Wrapper(v))| (k, v)).collect())
1281}
1282
1283fn deserialize_hex_color_vec<'de, D>(deserializer: D) -> Result<Vec<Color32>, D::Error>
1284where
1285 D: Deserializer<'de>,
1286{
1287 #[derive(Deserialize)]
1288 struct Wrapper(#[serde(deserialize_with = "deserialize_hex_color")] Color32);
1289
1290 let v = Vec::<Wrapper>::deserialize(deserializer)?;
1291 Ok(v.into_iter().map(|Wrapper(v)| v).collect())
1292}
1293
1294fn deserialize_theme<'de, D>(deserializer: D) -> Result<SurferTheme, D::Error>
1295where
1296 D: Deserializer<'de>,
1297{
1298 let buf = String::deserialize(deserializer)?;
1299 SurferTheme::new(Some(buf)).map_err(de::Error::custom)
1300}
1301
1302fn deserialize_non_negative_f32<'de, D>(deserializer: D) -> Result<f32, D::Error>
1303where
1304 D: Deserializer<'de>,
1305{
1306 let value = f32::deserialize(deserializer)?;
1307 Ok(value.max(0.0))
1308}
1309
1310fn deserialize_non_negative_f32_vec<'de, D>(deserializer: D) -> Result<Vec<f32>, D::Error>
1311where
1312 D: Deserializer<'de>,
1313{
1314 let values = Vec::<f32>::deserialize(deserializer)?;
1315 Ok(values.into_iter().map(|v| v.max(0.0)).collect())
1316}
1317
1318fn deserialize_unit_interval_f32<'de, D>(deserializer: D) -> Result<f32, D::Error>
1319where
1320 D: Deserializer<'de>,
1321{
1322 let value = f32::deserialize(deserializer)?;
1323 Ok(value.clamp(0.0, 1.0))
1324}
1325
1326#[cfg(not(target_arch = "wasm32"))]
1332#[must_use]
1333pub fn find_local_configs() -> Vec<Utf8PathBuf> {
1334 use crate::util::search_upward;
1335 match std::env::current_dir() {
1336 Ok(dir) => {
1337 let root = dir
1338 .ancestors()
1339 .last()
1340 .map_or_else(|| PathBuf::from("/"), Path::to_path_buf);
1341 search_upward(dir, root, LOCAL_DIR)
1342 .into_iter()
1343 .filter(|p| p.is_dir()) .rev() .collect()
1346 }
1347 Err(_) => vec![],
1348 }
1349}
1350
1351#[cfg(not(target_arch = "wasm32"))]
1352pub fn write_default_config() -> eyre::Result<()> {
1353 use std::fs;
1354
1355 let default_config = include_str!("../../default_config.toml");
1356
1357 if let Some(proj_dirs) = &*PROJECT_DIR {
1358 let config_dir = proj_dirs.config_dir();
1359 let config_path = Utf8PathBuf::from_path_buf(config_dir.join(CONFIG_FILE))
1360 .map_err(|p| eyre::eyre!("Config path '{}' contains invalid UTF-8", p.display()))?;
1361
1362 if config_path.exists() {
1363 return Err(eyre::eyre!(
1364 "Config file already exists at {config_path}. Delete it first if you want to recreate it."
1365 ));
1366 }
1367
1368 fs::create_dir_all(config_dir)?;
1369
1370 fs::write(&config_path, default_config)?;
1371
1372 info!("Default config written to {config_path}");
1373 }
1374
1375 Ok(())
1376}
1377
1378#[cfg(test)]
1379mod tests {
1380 use super::*;
1381
1382 #[test]
1383 fn test_hex_string_3_chars() {
1384 let result = hex_string_to_color32("abc").unwrap();
1386 let expected = Color32::from_rgb(0xaa, 0xbb, 0xcc);
1387 assert_eq!(result, expected);
1388 }
1389
1390 #[test]
1391 fn test_hex_string_6_chars() {
1392 let result = hex_string_to_color32("a7e47e").unwrap();
1394 let expected = Color32::from_rgb(0xa7, 0xe4, 0x7e);
1395 assert_eq!(result, expected);
1396 }
1397
1398 #[test]
1399 fn test_hex_string_black() {
1400 let result = hex_string_to_color32("000000").unwrap();
1402 let expected = Color32::from_rgb(0x00, 0x00, 0x00);
1403 assert_eq!(result, expected);
1404 }
1405
1406 #[test]
1407 fn test_hex_string_white() {
1408 let result = hex_string_to_color32("ffffff").unwrap();
1410 let expected = Color32::from_rgb(0xff, 0xff, 0xff);
1411 assert_eq!(result, expected);
1412 }
1413
1414 #[test]
1415 fn test_hex_string_uppercase() {
1416 let result = hex_string_to_color32("ABCDEF").unwrap();
1418 let expected = Color32::from_rgb(0xab, 0xcd, 0xef);
1419 assert_eq!(result, expected);
1420 }
1421
1422 #[test]
1423 fn test_hex_string_mixed_case() {
1424 let result = hex_string_to_color32("Ab5DeF").unwrap();
1426 let expected = Color32::from_rgb(0xab, 0x5d, 0xef);
1427 assert_eq!(result, expected);
1428 }
1429
1430 #[test]
1431 fn test_hex_string_invalid_length() {
1432 let result = hex_string_to_color32("ab");
1434 assert!(result.is_err());
1435
1436 let result = hex_string_to_color32("abcde");
1437 assert!(result.is_err());
1438
1439 let result = hex_string_to_color32("abcdefgh");
1440 assert!(result.is_err());
1441 }
1442
1443 #[test]
1444 fn test_hex_string_invalid_characters() {
1445 let result = hex_string_to_color32("GGGGGG");
1447 assert!(result.is_err());
1448
1449 let result = hex_string_to_color32("12345g");
1450 assert!(result.is_err());
1451
1452 let result = hex_string_to_color32("zzzzzz");
1453 assert!(result.is_err());
1454 }
1455
1456 #[test]
1457 fn test_hex_string_empty() {
1458 let result = hex_string_to_color32("");
1460 assert!(result.is_err());
1461 }
1462
1463 #[test]
1464 fn test_hex_string_3_chars_doubling() {
1465 let result = hex_string_to_color32("050").unwrap();
1467 let expected = Color32::from_rgb(0x00, 0x55, 0x00);
1468 assert_eq!(result, expected);
1469 }
1470
1471 #[test]
1472 fn test_hex_string_with_hash_3_chars() {
1473 let result = hex_string_to_color32("#abc").unwrap();
1474 let expected = Color32::from_rgb(0xaa, 0xbb, 0xcc);
1475 assert_eq!(result, expected);
1476 }
1477
1478 #[test]
1479 fn test_hex_string_with_hash_6_chars() {
1480 let result = hex_string_to_color32("#ABCDEF").unwrap();
1481 let expected = Color32::from_rgb(0xab, 0xcd, 0xef);
1482 assert_eq!(result, expected);
1483 }
1484}