Skip to main content

egui_components/
titlebar.rs

1//! `TitleBar` — a custom window title bar (for borderless / custom-chrome apps).
2//!
3//! Renders a draggable bar with the app title on the left, a closure for your
4//! own right-aligned content, and optional minimize / maximize / close buttons
5//! wired to the platform via [`egui::ViewportCommand`]. Double-clicking the bar
6//! toggles maximize; dragging it moves the window.
7//!
8//! ```ignore
9//! sc::TitleBar::new("My App").show(ui, |ui| {
10//!     ui.add(sc::Button::ghost("Help"));
11//! });
12//! ```
13
14use egui::{pos2, vec2, Align, Layout, Rect, Sense, Ui, UiBuilder, ViewportCommand};
15use egui_components_theme::Theme;
16
17use crate::icon::{paint_icon, IconKind};
18
19pub struct TitleBar {
20    title: String,
21    height: f32,
22    window_controls: bool,
23}
24
25impl TitleBar {
26    pub fn new(title: impl Into<String>) -> Self {
27        Self {
28            title: title.into(),
29            height: 38.0,
30            window_controls: true,
31        }
32    }
33    pub fn height(mut self, h: f32) -> Self {
34        self.height = h;
35        self
36    }
37    /// Hide the minimize / maximize / close buttons.
38    pub fn no_window_controls(mut self) -> Self {
39        self.window_controls = false;
40        self
41    }
42
43    pub fn show(self, ui: &mut Ui, right_content: impl FnOnce(&mut Ui)) {
44        let theme = Theme::get(ui.ctx());
45        let c = theme.colors;
46
47        let width = ui.available_width();
48        let (rect, bar_resp) =
49            ui.allocate_exact_size(vec2(width, self.height), Sense::click_and_drag());
50
51        // Window move / maximize via the bar background.
52        if bar_resp.drag_started() {
53            ui.ctx().send_viewport_cmd(ViewportCommand::StartDrag);
54        }
55        if bar_resp.double_clicked() {
56            let maximized = ui.input(|i| i.viewport().maximized.unwrap_or(false));
57            ui.ctx()
58                .send_viewport_cmd(ViewportCommand::Maximized(!maximized));
59        }
60
61        // Surface.
62        ui.painter().rect_filled(rect, 0.0, c.background);
63        ui.painter().line_segment(
64            [rect.left_bottom(), rect.right_bottom()],
65            theme.border_stroke(),
66        );
67
68        // Title (left).
69        ui.painter().text(
70            pos2(rect.left() + 14.0, rect.center().y),
71            egui::Align2::LEFT_CENTER,
72            &self.title,
73            egui::FontId::proportional(theme.metrics.font_size_md),
74            c.foreground,
75        );
76
77        // Window controls (right), then user content to their left.
78        let mut right_edge = rect.right();
79        if self.window_controls {
80            let btn_w = self.height * 1.2;
81            let close = control_button(ui, btn_rect(rect, right_edge, btn_w), IconKind::Close, true);
82            if close {
83                ui.ctx().send_viewport_cmd(ViewportCommand::Close);
84            }
85            right_edge -= btn_w;
86
87            let is_max = ui.input(|i| i.viewport().maximized.unwrap_or(false));
88            if maximize_button(ui, btn_rect(rect, right_edge, btn_w)) {
89                ui.ctx()
90                    .send_viewport_cmd(ViewportCommand::Maximized(!is_max));
91            }
92            right_edge -= btn_w;
93
94            if control_button(ui, btn_rect(rect, right_edge, btn_w), IconKind::Minus, false) {
95                ui.ctx().send_viewport_cmd(ViewportCommand::Minimized(true));
96            }
97            right_edge -= btn_w;
98        }
99
100        // User content region (right-aligned, left of the window controls).
101        let content_rect = Rect::from_min_max(
102            pos2(rect.left() + 120.0, rect.top()),
103            pos2(right_edge - 4.0, rect.bottom()),
104        );
105        if content_rect.width() > 0.0 {
106            let mut content = ui.new_child(
107                UiBuilder::new()
108                    .max_rect(content_rect)
109                    .layout(Layout::right_to_left(Align::Center)),
110            );
111            right_content(&mut content);
112        }
113    }
114}
115
116/// Maximize/restore button — drawn as a square outline (no matching icon glyph).
117fn maximize_button(ui: &mut Ui, rect: Rect) -> bool {
118    let theme = Theme::get(ui.ctx());
119    let c = theme.colors;
120    let resp = ui.interact(
121        rect,
122        ui.id().with(("titlebar-max", rect.center().x as i32)),
123        Sense::click(),
124    );
125    if ui.is_rect_visible(rect) {
126        let painter = ui.painter();
127        if resp.hovered() {
128            painter.rect_filled(rect, 0.0, c.accent_background);
129        }
130        let sq = Rect::from_center_size(rect.center(), vec2(11.0, 11.0));
131        painter.rect_stroke(
132            sq,
133            egui::CornerRadius::same(1),
134            egui::Stroke::new(1.5, c.foreground),
135            egui::StrokeKind::Inside,
136        );
137        if resp.hovered() {
138            ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand);
139        }
140    }
141    resp.clicked()
142}
143
144fn btn_rect(bar: Rect, right_edge: f32, w: f32) -> Rect {
145    Rect::from_min_max(
146        pos2(right_edge - w, bar.top()),
147        pos2(right_edge, bar.bottom()),
148    )
149}
150
151/// Returns true if clicked. `danger` tints the hover red (for close).
152fn control_button(ui: &mut Ui, rect: Rect, icon: IconKind, danger: bool) -> bool {
153    let theme = Theme::get(ui.ctx());
154    let c = theme.colors;
155    let resp = ui.interact(rect, ui.id().with(("titlebar-ctl", rect.center().x as i32)), Sense::click());
156    if ui.is_rect_visible(rect) {
157        let painter = ui.painter();
158        if resp.hovered() {
159            let bg = if danger {
160                c.danger_background
161            } else {
162                c.accent_background
163            };
164            painter.rect_filled(rect, 0.0, bg);
165        }
166        let fg = if danger && resp.hovered() {
167            c.danger_foreground
168        } else {
169            c.foreground
170        };
171        let ir = Rect::from_center_size(rect.center(), vec2(14.0, 14.0));
172        paint_icon(painter, icon, ir, fg, 1.5);
173        if resp.hovered() {
174            ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand);
175        }
176    }
177    resp.clicked()
178}