Skip to main content

egui_charts/ui_kit/ext/
ui_ext.rs

1//! UiExt - Extension trait for egui::Ui
2//!
3//! Convenience methods for common UI patterns.
4//!
5//! # Categories
6//!
7//! - **Icons**: small_icon, medium_icon, large_icon, icon_with_size
8//! - **Labels**: error_label, warning_label, success_label, info_label, muted_label, subdued_label, strong_label, monospace_label, code_label, label_with_icon, clickable_label
9//! - **Buttons**: primary_button, secondary_button, danger_button, icon_button, icon_button_with_tooltip, toggle_button
10//! - **Selection**: selectable_label_with_icon, selectable_value, radio_value
11//! - **Layout**: panel_content, section_header, subsection, list, separator_with_margin, themed_separator, full_width
12//! - **Spacing**: space_xs, space_sm, space_md, space_lg, space_xl
13//! - **Inputs**: text_edit_singleline, text_edit_multiline, number_edit, search_input
14//! - **Drag Values**: drag_value, drag_value_with_range, drag_angle
15//! - **Grid/Table**: grid_cell, grid_row, property_row
16//! - **Visibility**: visible_rect, is_visible
17//! - **Theming**: current_theme, is_dark_mode, is_light_mode
18//! - **Utility**: is_touch_mode, min_touch_target
19
20use crate::localization as l10n;
21use egui::{Color32, Rect, Response, RichText, Sense, TextEdit, Ui, Vec2, Widget};
22
23#[cfg(feature = "icons")]
24use crate::icons::Icon;
25#[cfg(feature = "icons")]
26use crate::styles::icons;
27use crate::theme::ThemeExt;
28use crate::tokens::DESIGN_TOKENS;
29
30/// Extension trait for `egui::Ui` with convenience methods for common UI patterns.
31pub trait UiExt {
32    // ==========================================================================
33    // Icon Methods (require "icons" feature)
34    // ==========================================================================
35
36    /// Render a small icon (16px)
37    #[cfg(feature = "icons")]
38    fn small_icon(&mut self, icon: &Icon) -> Response;
39
40    /// Render a medium icon (20px)
41    #[cfg(feature = "icons")]
42    fn medium_icon(&mut self, icon: &Icon) -> Response;
43
44    /// Render a large icon (24px)
45    #[cfg(feature = "icons")]
46    fn large_icon(&mut self, icon: &Icon) -> Response;
47
48    /// Render an icon at a specific size
49    #[cfg(feature = "icons")]
50    fn icon_with_size(&mut self, icon: &Icon, size: f32) -> Response;
51
52    // ==========================================================================
53    // Label Methods
54    // ==========================================================================
55
56    /// Render an error label (red text)
57    fn error_label(&mut self, text: impl Into<String>) -> Response;
58
59    /// Render a warning label (orange text)
60    fn warning_label(&mut self, text: impl Into<String>) -> Response;
61
62    /// Render a success label (green text)
63    fn success_label(&mut self, text: impl Into<String>) -> Response;
64
65    /// Render an info label (blue text)
66    fn info_label(&mut self, text: impl Into<String>) -> Response;
67
68    /// Render a muted/secondary label (subdued text)
69    fn muted_label(&mut self, text: impl Into<String>) -> Response;
70
71    /// Render a subdued label (alias for muted_label)
72    fn subdued_label(&mut self, text: impl Into<String>) -> Response;
73
74    /// Render a hint label (small + weak, for column headers and auxiliary info)
75    fn hint_label(&mut self, text: impl Into<String>) -> Response;
76
77    /// Render a strong/bold label
78    fn strong_label(&mut self, text: impl Into<String>) -> Response;
79
80    /// Render a monospace label
81    fn monospace_label(&mut self, text: impl Into<String>) -> Response;
82
83    /// Render a code-style label (monospace with background)
84    fn code_label(&mut self, text: impl Into<String>) -> Response;
85
86    /// Render a label with an icon prefix
87    #[cfg(feature = "icons")]
88    fn label_with_icon(&mut self, icon: &Icon, text: impl Into<String>) -> Response;
89
90    /// Render a clickable label (looks like link)
91    fn clickable_label(&mut self, text: impl Into<String>) -> Response;
92
93    // ==========================================================================
94    // Button Methods
95    // ==========================================================================
96
97    /// Render a primary button (high emphasis)
98    fn primary_button(&mut self, text: impl Into<String>) -> Response;
99
100    /// Render a secondary button (medium emphasis)
101    fn secondary_button(&mut self, text: impl Into<String>) -> Response;
102
103    /// Render a danger button (destructive action)
104    fn danger_button(&mut self, text: impl Into<String>) -> Response;
105
106    /// Render an icon-only button
107    #[cfg(feature = "icons")]
108    fn icon_button(&mut self, icon: &Icon) -> Response;
109
110    /// Render an icon button with tooltip
111    #[cfg(feature = "icons")]
112    fn icon_button_with_tooltip(&mut self, icon: &Icon, tooltip: impl Into<String>) -> Response;
113
114    /// Render a toggle button
115    fn toggle_button(&mut self, selected: &mut bool, text: impl Into<String>) -> Response;
116
117    // ==========================================================================
118    // Selection Methods
119    // ==========================================================================
120
121    /// Render a selectable label with an icon
122    #[cfg(feature = "icons")]
123    fn selectable_label_with_icon(
124        &mut self,
125        selected: bool,
126        icon: &Icon,
127        text: impl Into<String>,
128    ) -> Response;
129
130    /// Render a selectable value (like radio button without the circle)
131    fn selectable_value_ui<T: PartialEq + Clone>(
132        &mut self,
133        current: &mut T,
134        value: T,
135        text: impl Into<String>,
136    ) -> Response;
137
138    /// Render a radio-style value
139    fn radio_value_ui<T: PartialEq + Clone>(
140        &mut self,
141        current: &mut T,
142        value: T,
143        text: impl Into<String>,
144    ) -> Response;
145
146    /// Combo box for selecting from typed options.
147    ///
148    /// Replaces the 11-line `ComboBox::from_id_salt → .show_ui → for … selectable_value` pattern.
149    /// Returns `true` if the value changed.
150    ///
151    /// ```
152    /// # use egui_charts::ui_kit::ext::UiExt;
153    /// # egui::__run_test_ui(|ui| {
154    /// let mut theme = "dark";
155    /// let names = egui_charts::theme::presets().map(|(name, _)| name);
156    /// let changed = ui.combo_select("theme", &mut theme, names, |n| n.to_string());
157    /// # });
158    /// ```
159    fn combo_select<T: PartialEq + Clone>(
160        &mut self,
161        id_salt: impl std::hash::Hash + std::fmt::Debug,
162        value: &mut T,
163        options: impl IntoIterator<Item = T>,
164        display: impl Fn(&T) -> String,
165    ) -> bool;
166
167    /// Combo box for selecting from typed options with explicit width.
168    fn combo_select_width<T: PartialEq + Clone>(
169        &mut self,
170        id_salt: impl std::hash::Hash + std::fmt::Debug,
171        value: &mut T,
172        options: impl IntoIterator<Item = T>,
173        display: impl Fn(&T) -> String,
174        width: f32,
175    ) -> bool;
176
177    /// Combo box for selecting a `String` value from `&str` options.
178    ///
179    /// ```
180    /// # use egui_charts::ui_kit::ext::UiExt;
181    /// # egui::__run_test_ui(|ui| {
182    /// # let mut condition = String::from("Crossing");
183    /// let changed = ui.combo_str_select("condition", &mut condition, &["Crossing", "Greater Than", "Less Than"]);
184    /// # });
185    /// ```
186    fn combo_str_select(
187        &mut self,
188        id_salt: impl std::hash::Hash + std::fmt::Debug,
189        value: &mut String,
190        options: &[&str],
191    ) -> bool;
192
193    /// Combo box for selecting a `String` value from `&str` options with explicit width.
194    fn combo_str_select_width(
195        &mut self,
196        id_salt: impl std::hash::Hash + std::fmt::Debug,
197        value: &mut String,
198        options: &[&str],
199        width: f32,
200    ) -> bool;
201
202    // ==========================================================================
203    // Layout Methods
204    // ==========================================================================
205
206    /// Add content within a panel-style container
207    fn panel_content(&mut self, add_contents: impl FnOnce(&mut Ui));
208
209    /// Render a section header
210    fn section_header(&mut self, text: impl Into<String>);
211
212    /// Render a small uppercase section label (e.g. "BALANCE", "MARGIN").
213    fn section_label(&mut self, text: impl Into<String>) -> Response;
214
215    /// Render a subsection with collapsible header
216    fn subsection(&mut self, text: impl Into<String>, add_contents: impl FnOnce(&mut Ui));
217
218    /// Render content in a list-style container
219    fn list(&mut self, add_contents: impl FnOnce(&mut Ui));
220
221    /// Right-align content within the current horizontal layout.
222    ///
223    /// Replaces `ui.with_layout(Layout::right_to_left(Align::Center), |ui| { ... })`.
224    fn right_aligned(&mut self, add_contents: impl FnOnce(&mut Ui));
225
226    /// Vertical scroll area that fills available space.
227    ///
228    /// Replaces `egui::ScrollArea::vertical().auto_shrink([false, false]).show(ui, ...)`.
229    fn scroll_vertical(&mut self, add_contents: impl FnOnce(&mut Ui));
230
231    /// Add a separator with margin
232    fn separator_with_margin(&mut self, margin: f32);
233
234    /// Add a separator with small spacing above and below, the usual section divider.
235    fn spaced_separator(&mut self);
236
237    /// Add a horizontal separator with theme-appropriate color
238    fn themed_separator(&mut self);
239
240    /// Vertical separator for top toolbar
241    /// 1px width, 20px height, 12px padding each side
242    fn toolbar_separator(&mut self);
243
244    /// Get full available width
245    fn full_width(&self) -> f32;
246
247    // ==========================================================================
248    // Spacing Methods
249    // ==========================================================================
250
251    /// Add extra small spacing (2px)
252    fn space_xs(&mut self);
253
254    /// Add small spacing (4px)
255    fn space_sm(&mut self);
256
257    /// Add medium spacing (6px)
258    fn space_md(&mut self);
259
260    /// Add large spacing (8px)
261    fn space_lg(&mut self);
262
263    /// Add extra large spacing (12px)
264    fn space_xl(&mut self);
265
266    /// Add extra extra large spacing (16px)
267    fn space_xxl(&mut self);
268
269    // Legacy aliases
270    /// Add small spacing (4px) - alias for space_sm
271    fn small_space(&mut self);
272
273    /// Add medium spacing (8px) - alias for space_lg
274    fn medium_space(&mut self);
275
276    /// Add large spacing (12px) - alias for space_xl
277    fn large_space(&mut self);
278
279    // ==========================================================================
280    // Input Methods
281    // ==========================================================================
282
283    /// Single-line text edit
284    fn text_edit_singleline_ui(&mut self, text: &mut String) -> Response;
285
286    /// Multi-line text edit
287    fn text_edit_multiline_ui(&mut self, text: &mut String) -> Response;
288
289    /// Number edit field
290    fn number_edit(&mut self, value: &mut f64) -> Response;
291
292    /// Search input with icon
293    fn search_input(&mut self, query: &mut String) -> Response;
294
295    // ==========================================================================
296    // Drag Value Methods
297    // ==========================================================================
298
299    /// Drag value for f32
300    fn drag_value_f32(&mut self, value: &mut f32) -> Response;
301
302    /// Drag value with range
303    fn drag_value_with_range(
304        &mut self,
305        value: &mut f32,
306        range: std::ops::RangeInclusive<f32>,
307    ) -> Response;
308
309    /// Drag angle (radians, displayed as degrees)
310    fn drag_angle(&mut self, radians: &mut f32) -> Response;
311
312    // ==========================================================================
313    // Grid/Table Methods
314    // ==========================================================================
315
316    /// Render a property row (label: value)
317    fn property_row(&mut self, label: impl Into<String>, add_value: impl FnOnce(&mut Ui));
318
319    // ==========================================================================
320    // Visibility Methods
321    // ==========================================================================
322
323    /// Get the visible rect
324    fn visible_rect(&self) -> Rect;
325
326    /// Check if the UI is visible
327    fn is_visible(&self) -> bool;
328
329    // ==========================================================================
330    // Theming Methods
331    // ==========================================================================
332
333    /// Get the current theme
334    fn current_theme(&self) -> egui::Theme;
335
336    /// Check if dark mode is active
337    fn is_dark_mode(&self) -> bool;
338
339    /// Check if light mode is active
340    fn is_light_mode(&self) -> bool;
341
342    // ==========================================================================
343    // Utility Methods
344    // ==========================================================================
345
346    /// Check if touch input mode is active
347    fn is_touch_mode(&self) -> bool;
348
349    /// Get minimum touch target size for current input mode
350    fn min_touch_target(&self) -> f32;
351
352    // ==========================================================================
353    // Trading Color Methods
354    // ==========================================================================
355
356    /// Get P&L color: bullish for positive, bearish for negative, neutral for zero
357    fn pnl_color(&self, value: f64) -> Color32;
358}
359
360impl UiExt for Ui {
361    // ==========================================================================
362    // Icon Methods (require "icons" feature)
363    // ==========================================================================
364
365    #[cfg(feature = "icons")]
366    fn small_icon(&mut self, icon: &Icon) -> Response {
367        self.icon_with_size(icon, icons::SMALL)
368    }
369
370    #[cfg(feature = "icons")]
371    fn medium_icon(&mut self, icon: &Icon) -> Response {
372        self.icon_with_size(icon, icons::MEDIUM)
373    }
374
375    #[cfg(feature = "icons")]
376    fn large_icon(&mut self, icon: &Icon) -> Response {
377        self.icon_with_size(icon, icons::LARGE)
378    }
379
380    #[cfg(feature = "icons")]
381    fn icon_with_size(&mut self, icon: &Icon, size: f32) -> Response {
382        let tint = self.style().visuals.widgets.noninteractive.fg_stroke.color;
383        let image = icon.as_image_tinted(Vec2::splat(size), tint);
384        self.add(image)
385    }
386
387    // ==========================================================================
388    // Label Methods
389    // ==========================================================================
390
391    fn error_label(&mut self, text: impl Into<String>) -> Response {
392        let color = self.active_theme().ui.error;
393        self.colored_label(color, l10n::text(self.ctx(), Into::<String>::into(text)))
394    }
395
396    fn warning_label(&mut self, text: impl Into<String>) -> Response {
397        let color = self.active_theme().ui.warning;
398        self.colored_label(color, l10n::text(self.ctx(), Into::<String>::into(text)))
399    }
400
401    fn success_label(&mut self, text: impl Into<String>) -> Response {
402        let color = self.active_theme().ui.success;
403        self.colored_label(color, l10n::text(self.ctx(), Into::<String>::into(text)))
404    }
405
406    fn info_label(&mut self, text: impl Into<String>) -> Response {
407        let color = self.active_theme().ui.info;
408        self.colored_label(color, l10n::text(self.ctx(), Into::<String>::into(text)))
409    }
410
411    fn muted_label(&mut self, text: impl Into<String>) -> Response {
412        let color = self.style().visuals.widgets.noninteractive.fg_stroke.color;
413        self.colored_label(color, l10n::text(self.ctx(), Into::<String>::into(text)))
414    }
415
416    fn subdued_label(&mut self, text: impl Into<String>) -> Response {
417        self.muted_label(text)
418    }
419
420    fn hint_label(&mut self, text: impl Into<String>) -> Response {
421        self.label(
422            RichText::new(l10n::text(self.ctx(), Into::<String>::into(text)))
423                .small()
424                .weak(),
425        )
426    }
427
428    fn strong_label(&mut self, text: impl Into<String>) -> Response {
429        self.label(RichText::new(l10n::text(self.ctx(), Into::<String>::into(text))).strong())
430    }
431
432    fn monospace_label(&mut self, text: impl Into<String>) -> Response {
433        self.label(RichText::new(l10n::text(self.ctx(), Into::<String>::into(text))).monospace())
434    }
435
436    fn code_label(&mut self, text: impl Into<String>) -> Response {
437        self.code(text.into())
438    }
439
440    #[cfg(feature = "icons")]
441    fn label_with_icon(&mut self, icon: &Icon, text: impl Into<String>) -> Response {
442        let text = l10n::text(self.ctx(), Into::<String>::into(text));
443        let icon_size = icons::SMALL;
444
445        self.horizontal(|ui| {
446            let tint = ui.style().visuals.widgets.noninteractive.fg_stroke.color;
447            ui.add(icon.as_image_tinted(Vec2::splat(icon_size), tint));
448            ui.label(text);
449        })
450        .response
451    }
452
453    fn clickable_label(&mut self, text: impl Into<String>) -> Response {
454        let text = l10n::text(self.ctx(), Into::<String>::into(text));
455        let color = self.visuals().hyperlink_color;
456        self.add(egui::Label::new(RichText::new(text).color(color)).sense(Sense::click()))
457    }
458
459    // ==========================================================================
460    // Button Methods
461    // ==========================================================================
462
463    fn primary_button(&mut self, text: impl Into<String>) -> Response {
464        self.add(crate::ui_kit::buttons::Button::new(text).primary())
465    }
466
467    fn secondary_button(&mut self, text: impl Into<String>) -> Response {
468        self.add(crate::ui_kit::buttons::Button::new(text).secondary())
469    }
470
471    fn danger_button(&mut self, text: impl Into<String>) -> Response {
472        self.add(crate::ui_kit::buttons::Button::new(text).danger())
473    }
474
475    #[cfg(feature = "icons")]
476    fn icon_button(&mut self, icon: &Icon) -> Response {
477        let size = DESIGN_TOKENS.sizing.button_md;
478        let icon_size = icons::MEDIUM;
479        let (rect, response) = self.allocate_exact_size(Vec2::splat(size), Sense::click());
480        crate::styles::focus::draw_focus_ring(self, &response, self.active_theme().radius.sm);
481
482        if self.is_rect_visible(rect) {
483            let visuals = &self.style().visuals;
484            let bg_color = if response.is_pointer_button_down_on() {
485                visuals.widgets.active.bg_fill
486            } else if response.hovered() {
487                visuals.widgets.hovered.bg_fill
488            } else {
489                Color32::TRANSPARENT
490            };
491
492            self.painter()
493                .rect_filled(rect, self.active_theme().radius.sm, bg_color);
494
495            let icon_rect = Rect::from_center_size(rect.center(), Vec2::splat(icon_size));
496            let icon_color = if response.hovered() {
497                visuals.widgets.hovered.fg_stroke.color
498            } else {
499                visuals.widgets.noninteractive.fg_stroke.color
500            };
501            icon.as_image_tinted(Vec2::splat(icon_size), icon_color)
502                .paint_at(self, icon_rect);
503        }
504
505        response
506    }
507
508    #[cfg(feature = "icons")]
509    fn icon_button_with_tooltip(&mut self, icon: &Icon, tooltip: impl Into<String>) -> Response {
510        let response = self.icon_button(icon);
511        response.on_hover_text(l10n::text(self.ctx(), Into::<String>::into(tooltip)))
512    }
513
514    fn toggle_button(&mut self, selected: &mut bool, text: impl Into<String>) -> Response {
515        let text = l10n::text(self.ctx(), Into::<String>::into(text));
516        let visuals = &self.style().visuals;
517
518        let (bg, fg) = if *selected {
519            (visuals.selection.bg_fill, visuals.selection.stroke.color)
520        } else {
521            (
522                Color32::TRANSPARENT,
523                visuals.widgets.inactive.fg_stroke.color,
524            )
525        };
526
527        let button = egui::Button::new(RichText::new(text).color(fg))
528            .fill(bg)
529            .corner_radius(self.active_theme().radius.sm);
530
531        let response = self.add(button);
532        if response.clicked() {
533            *selected = !*selected;
534        }
535        response
536    }
537
538    // ==========================================================================
539    // Selection Methods
540    // ==========================================================================
541
542    #[cfg(feature = "icons")]
543    fn selectable_label_with_icon(
544        &mut self,
545        selected: bool,
546        icon: &Icon,
547        text: impl Into<String>,
548    ) -> Response {
549        let text = l10n::text(self.ctx(), Into::<String>::into(text));
550        let icon_size = icons::SMALL;
551        let total_width = self.available_width();
552
553        let (rect, response) = self.allocate_exact_size(
554            Vec2::new(total_width, DESIGN_TOKENS.sizing.button_md),
555            Sense::click(),
556        );
557        crate::styles::focus::draw_focus_ring(self, &response, self.active_theme().radius.sm);
558
559        if self.is_rect_visible(rect) {
560            let visuals = self.style().visuals.clone();
561
562            // Background
563            let bg_color = if selected {
564                visuals.selection.bg_fill
565            } else if response.hovered() {
566                visuals.widgets.hovered.bg_fill
567            } else {
568                Color32::TRANSPARENT
569            };
570
571            self.painter()
572                .rect_filled(rect, self.active_theme().radius.sm, bg_color);
573
574            // Icon
575            let icon_rect = Rect::from_min_size(
576                rect.min
577                    + Vec2::new(
578                        self.active_theme().spacing.sm,
579                        (rect.height() - icon_size) / 2.0,
580                    ),
581                Vec2::splat(icon_size),
582            );
583            let icon_color = if selected {
584                visuals.selection.stroke.color
585            } else {
586                visuals.widgets.noninteractive.fg_stroke.color
587            };
588            icon.as_image_tinted(Vec2::splat(icon_size), icon_color)
589                .paint_at(self, icon_rect);
590
591            // Text
592            let text_pos = egui::pos2(
593                icon_rect.right() + self.active_theme().spacing.sm,
594                rect.center().y - self.text_style_height(&egui::TextStyle::Body) / 2.0,
595            );
596            let text_color = if selected {
597                visuals.selection.stroke.color
598            } else {
599                visuals.widgets.noninteractive.fg_stroke.color
600            };
601            self.painter().text(
602                text_pos,
603                egui::Align2::LEFT_TOP,
604                text,
605                egui::TextStyle::Body.resolve(self.style()),
606                text_color,
607            );
608        }
609
610        response
611    }
612
613    fn selectable_value_ui<T: PartialEq + Clone>(
614        &mut self,
615        current: &mut T,
616        value: T,
617        text: impl Into<String>,
618    ) -> Response {
619        let selected = *current == value;
620        let response =
621            self.selectable_label(selected, l10n::text(self.ctx(), Into::<String>::into(text)));
622        if response.clicked() {
623            *current = value;
624        }
625        response
626    }
627
628    fn radio_value_ui<T: PartialEq + Clone>(
629        &mut self,
630        current: &mut T,
631        value: T,
632        text: impl Into<String>,
633    ) -> Response {
634        let selected = *current == value;
635        let response = self.radio(selected, l10n::text(self.ctx(), Into::<String>::into(text)));
636        if response.clicked() {
637            *current = value;
638        }
639        response
640    }
641
642    fn combo_select<T: PartialEq + Clone>(
643        &mut self,
644        id_salt: impl std::hash::Hash + std::fmt::Debug,
645        value: &mut T,
646        options: impl IntoIterator<Item = T>,
647        display: impl Fn(&T) -> String,
648    ) -> bool {
649        let selected_text = l10n::text(self.ctx(), display(value));
650        let mut changed = false;
651        egui::ComboBox::from_id_salt(id_salt)
652            .icon(crate::ui_kit::buttons::combo_chevron)
653            .selected_text(selected_text)
654            .show_ui(self, |ui| {
655                for option in options {
656                    let label = l10n::text(ui.ctx(), display(&option));
657                    if ui.selectable_value(value, option, label).changed() {
658                        changed = true;
659                    }
660                }
661            });
662        changed
663    }
664
665    fn combo_select_width<T: PartialEq + Clone>(
666        &mut self,
667        id_salt: impl std::hash::Hash + std::fmt::Debug,
668        value: &mut T,
669        options: impl IntoIterator<Item = T>,
670        display: impl Fn(&T) -> String,
671        width: f32,
672    ) -> bool {
673        let selected_text = l10n::text(self.ctx(), display(value));
674        let mut changed = false;
675        egui::ComboBox::from_id_salt(id_salt)
676            .icon(crate::ui_kit::buttons::combo_chevron)
677            .selected_text(selected_text)
678            .width(width)
679            .show_ui(self, |ui| {
680                for option in options {
681                    let label = l10n::text(ui.ctx(), display(&option));
682                    if ui.selectable_value(value, option, label).changed() {
683                        changed = true;
684                    }
685                }
686            });
687        changed
688    }
689
690    fn combo_str_select(
691        &mut self,
692        id_salt: impl std::hash::Hash + std::fmt::Debug,
693        value: &mut String,
694        options: &[&str],
695    ) -> bool {
696        let mut changed = false;
697        egui::ComboBox::from_id_salt(id_salt)
698            .icon(crate::ui_kit::buttons::combo_chevron)
699            .selected_text(l10n::text(self.ctx(), value.as_str()))
700            .show_ui(self, |ui| {
701                for &option in options {
702                    if ui
703                        .selectable_value(value, option.to_string(), l10n::text(ui.ctx(), option))
704                        .changed()
705                    {
706                        changed = true;
707                    }
708                }
709            });
710        changed
711    }
712
713    fn combo_str_select_width(
714        &mut self,
715        id_salt: impl std::hash::Hash + std::fmt::Debug,
716        value: &mut String,
717        options: &[&str],
718        width: f32,
719    ) -> bool {
720        let mut changed = false;
721        egui::ComboBox::from_id_salt(id_salt)
722            .icon(crate::ui_kit::buttons::combo_chevron)
723            .selected_text(l10n::text(self.ctx(), value.as_str()))
724            .width(width)
725            .show_ui(self, |ui| {
726                for &option in options {
727                    if ui
728                        .selectable_value(value, option.to_string(), l10n::text(ui.ctx(), option))
729                        .changed()
730                    {
731                        changed = true;
732                    }
733                }
734            });
735        changed
736    }
737
738    // ==========================================================================
739    // Layout Methods
740    // ==========================================================================
741
742    fn panel_content(&mut self, add_contents: impl FnOnce(&mut Ui)) {
743        egui::Frame::new()
744            .inner_margin(egui::Margin::same(self.active_theme().spacing.md as i8))
745            .show(self, |ui| {
746                add_contents(ui);
747            });
748    }
749
750    fn section_header(&mut self, text: impl Into<String>) {
751        let text = l10n::text(self.ctx(), Into::<String>::into(text));
752        self.add_space(self.active_theme().spacing.lg);
753        self.label(
754            RichText::new(text)
755                .strong()
756                .size(self.active_theme().text.body),
757        );
758        self.add_space(self.active_theme().spacing.sm);
759    }
760
761    fn section_label(&mut self, text: impl Into<String>) -> Response {
762        let color = self.visuals().widgets.noninteractive.fg_stroke.color;
763        self.label(
764            RichText::new(l10n::text(self.ctx(), Into::<String>::into(text)))
765                .size(self.active_theme().text.marker)
766                .strong()
767                .color(color),
768        )
769    }
770
771    fn subsection(&mut self, text: impl Into<String>, add_contents: impl FnOnce(&mut Ui)) {
772        let text = text.into();
773        egui::CollapsingHeader::new(l10n::text(self.ctx(), &text))
774            .id_salt(&text)
775            .default_open(true)
776            .show(self, |ui| {
777                add_contents(ui);
778            });
779    }
780
781    fn right_aligned(&mut self, add_contents: impl FnOnce(&mut Ui)) {
782        self.with_layout(
783            egui::Layout::right_to_left(egui::Align::Center),
784            add_contents,
785        );
786    }
787
788    fn scroll_vertical(&mut self, add_contents: impl FnOnce(&mut Ui)) {
789        egui::ScrollArea::vertical()
790            .auto_shrink([false, false])
791            .show(self, |ui| {
792                add_contents(ui);
793            });
794    }
795
796    fn list(&mut self, add_contents: impl FnOnce(&mut Ui)) {
797        self.vertical(|ui| {
798            ui.spacing_mut().item_spacing.y = ui.active_theme().spacing.xs;
799            add_contents(ui);
800        });
801    }
802
803    fn separator_with_margin(&mut self, margin: f32) {
804        self.add_space(margin);
805        self.separator();
806        self.add_space(margin);
807    }
808
809    fn spaced_separator(&mut self) {
810        self.add_space(self.active_theme().spacing.sm);
811        self.separator();
812        self.add_space(self.active_theme().spacing.sm);
813    }
814
815    fn themed_separator(&mut self) {
816        let stroke_color = self.style().visuals.widgets.noninteractive.bg_stroke.color;
817        let rect = self.available_rect_before_wrap();
818        let y = rect.top();
819        self.painter().hline(
820            rect.x_range(),
821            y,
822            egui::Stroke::new(DESIGN_TOKENS.stroke.hairline, stroke_color),
823        );
824        self.add_space(DESIGN_TOKENS.stroke.hairline);
825    }
826
827    fn toolbar_separator(&mut self) {
828        let padding = self.active_theme().spacing.xl; // 12px
829        let sep_width = DESIGN_TOKENS.sizing.toolbar.separator_width;
830        let sep_height = DESIGN_TOKENS.sizing.toolbar.separator_height;
831        self.add_space(padding);
832        let (rect, _) = self.allocate_exact_size(Vec2::new(sep_width, sep_height), Sense::hover());
833        if self.is_rect_visible(rect) {
834            self.painter().rect_filled(
835                rect,
836                0.0,
837                self.visuals().widgets.noninteractive.bg_stroke.color,
838            );
839        }
840        self.add_space(padding);
841    }
842
843    fn full_width(&self) -> f32 {
844        self.available_width()
845    }
846
847    // ==========================================================================
848    // Spacing Methods
849    // ==========================================================================
850
851    fn space_xs(&mut self) {
852        self.add_space(self.active_theme().spacing.xs);
853    }
854
855    fn space_sm(&mut self) {
856        self.add_space(self.active_theme().spacing.sm);
857    }
858
859    fn space_md(&mut self) {
860        self.add_space(self.active_theme().spacing.md);
861    }
862
863    fn space_lg(&mut self) {
864        self.add_space(self.active_theme().spacing.lg);
865    }
866
867    fn space_xl(&mut self) {
868        self.add_space(self.active_theme().spacing.xl);
869    }
870
871    fn space_xxl(&mut self) {
872        self.add_space(self.active_theme().spacing.xxl);
873    }
874
875    // Legacy aliases
876    fn small_space(&mut self) {
877        self.space_sm();
878    }
879
880    fn medium_space(&mut self) {
881        self.space_lg();
882    }
883
884    fn large_space(&mut self) {
885        self.space_xl();
886    }
887
888    // ==========================================================================
889    // Input Methods
890    // ==========================================================================
891
892    fn text_edit_singleline_ui(&mut self, text: &mut String) -> Response {
893        TextEdit::singleline(text)
894            .desired_width(f32::INFINITY)
895            .ui(self)
896    }
897
898    fn text_edit_multiline_ui(&mut self, text: &mut String) -> Response {
899        TextEdit::multiline(text)
900            .desired_width(f32::INFINITY)
901            .ui(self)
902    }
903
904    fn number_edit(&mut self, value: &mut f64) -> Response {
905        self.add(egui::DragValue::new(value).speed(0.1))
906    }
907
908    fn search_input(&mut self, query: &mut String) -> Response {
909        let placeholder = l10n::text(self.ctx(), "Search...");
910        let response = TextEdit::singleline(query)
911            .hint_text(&placeholder)
912            .desired_width(f32::INFINITY)
913            .ui(self)
914            .accessible_name(placeholder);
915
916        // Clear button when not empty
917        if !query.is_empty()
918            && response.lost_focus()
919            && self.input(|i| i.key_pressed(egui::Key::Escape))
920        {
921            query.clear();
922        }
923
924        response
925    }
926
927    // ==========================================================================
928    // Drag Value Methods
929    // ==========================================================================
930
931    fn drag_value_f32(&mut self, value: &mut f32) -> Response {
932        self.add(egui::DragValue::new(value).speed(0.1))
933    }
934
935    fn drag_value_with_range(
936        &mut self,
937        value: &mut f32,
938        range: std::ops::RangeInclusive<f32>,
939    ) -> Response {
940        self.add(egui::DragValue::new(value).speed(0.1).range(range))
941    }
942
943    fn drag_angle(&mut self, radians: &mut f32) -> Response {
944        self.add(
945            egui::DragValue::new(radians)
946                .speed(0.01)
947                .suffix("°")
948                .custom_formatter(|n, _| format!("{:.1}", n.to_degrees()))
949                .custom_parser(|s| s.parse::<f64>().ok().map(|d| d.to_radians())),
950        )
951    }
952
953    // ==========================================================================
954    // Grid/Table Methods
955    // ==========================================================================
956
957    fn property_row(&mut self, label: impl Into<String>, add_value: impl FnOnce(&mut Ui)) {
958        self.horizontal(|ui| {
959            ui.label(l10n::text(ui.ctx(), Into::<String>::into(label)));
960            ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
961                add_value(ui);
962            });
963        });
964    }
965
966    // ==========================================================================
967    // Visibility Methods
968    // ==========================================================================
969
970    fn visible_rect(&self) -> Rect {
971        self.clip_rect()
972    }
973
974    fn is_visible(&self) -> bool {
975        let rect = self.clip_rect();
976        rect.width() > 0.0 && rect.height() > 0.0
977    }
978
979    // ==========================================================================
980    // Theming Methods
981    // ==========================================================================
982
983    fn current_theme(&self) -> egui::Theme {
984        self.ctx().theme()
985    }
986
987    fn is_dark_mode(&self) -> bool {
988        self.ctx().theme() == egui::Theme::Dark
989    }
990
991    fn is_light_mode(&self) -> bool {
992        self.ctx().theme() == egui::Theme::Light
993    }
994
995    // ==========================================================================
996    // Utility Methods
997    // ==========================================================================
998
999    fn is_touch_mode(&self) -> bool {
1000        self.ctx().input(|i| i.any_touches())
1001    }
1002
1003    fn min_touch_target(&self) -> f32 {
1004        DESIGN_TOKENS.sizing.target_min
1005    }
1006
1007    // ==========================================================================
1008    // Trading Color Methods
1009    // ==========================================================================
1010
1011    fn pnl_color(&self, value: f64) -> Color32 {
1012        if value > 0.0 {
1013            self.active_theme().chart.candle_up
1014        } else if value < 0.0 {
1015            self.active_theme().chart.candle_down
1016        } else {
1017            self.style().visuals.text_color()
1018        }
1019    }
1020}