Skip to main content

khora_editor/widgets/
chrome.rs

1// Copyright 2025 eraflo
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Chrome widgets — pieces shared between title bar, status bar and panel
16//! headers. Everything below paints to absolute screen-space coordinates and
17//! tracks its own click hit-test via [`UiBuilder::interact_rect`].
18
19use khora_sdk::editor_ui::{FontFamilyHint, Icon, Interaction, TextAlign, UiBuilder, UiTheme};
20// `Icon` is consumed inside `paint_search_pill` via `Icon::Search`.
21// `Interaction` is the return type of `paint_search_pill` and `panel_tab` (for
22// the click + width tuple). Kept explicit to make the public surface readable.
23
24use super::paint::{paint_icon, paint_text_size, with_alpha};
25
26/// A keyboard-shortcut chip ("⌘", "K", "Esc"). Returns the right edge in
27/// screen-space.
28pub fn paint_kbd_chip(
29    ui: &mut dyn UiBuilder,
30    origin: [f32; 2],
31    label: &str,
32    theme: &UiTheme,
33) -> f32 {
34    let measured = ui.measure_text(label, 10.5, FontFamilyHint::Monospace)[0];
35    let w = measured.max(14.0) + 10.0;
36    let h = 16.0;
37    ui.paint_rect_filled(origin, [w, h], theme.surface_active, 3.0);
38    ui.paint_rect_stroke(origin, [w, h], theme.border, 3.0, 1.0);
39    // Bottom shadow line for the "depth"
40    ui.paint_line(
41        [origin[0] + 1.0, origin[1] + h - 0.5],
42        [origin[0] + w - 1.0, origin[1] + h - 0.5],
43        with_alpha(theme.border, 0.5),
44        1.0,
45    );
46    ui.paint_text_styled(
47        [origin[0] + w * 0.5, origin[1] + 2.0],
48        label,
49        10.5,
50        theme.text_dim,
51        FontFamilyHint::Monospace,
52        TextAlign::Center,
53    );
54    origin[0] + w
55}
56
57/// A circular dot indicator (used in status bars and pill chips).
58pub fn paint_status_dot(ui: &mut dyn UiBuilder, center: [f32; 2], color: [f32; 4]) {
59    ui.paint_circle_filled(center, 3.5, color);
60    ui.paint_circle_filled(center, 5.5, with_alpha(color, 0.18));
61}
62
63/// The wide "search commands" pill in the title bar. Returns the click
64/// interaction (whether the user wants to open the command palette) plus the
65/// right edge in screen-space.
66pub fn paint_search_pill(
67    ui: &mut dyn UiBuilder,
68    origin: [f32; 2],
69    width: f32,
70    height: f32,
71    placeholder: &str,
72    theme: &UiTheme,
73) -> (Interaction, f32) {
74    ui.paint_rect_filled(origin, [width, height], theme.surface_elevated, 6.0);
75    ui.paint_rect_stroke(
76        origin,
77        [width, height],
78        with_alpha(theme.separator, 0.6),
79        6.0,
80        1.0,
81    );
82
83    let cy = origin[1] + height * 0.5;
84    paint_icon(
85        ui,
86        [origin[0] + 10.0, cy - 6.5],
87        Icon::Search,
88        13.0,
89        theme.text_dim,
90    );
91
92    paint_text_size(
93        ui,
94        [
95            origin[0] + 30.0,
96            origin[1] + (height - theme.font_size_body) * 0.5,
97        ],
98        placeholder,
99        theme.font_size_body - 0.5,
100        theme.text_muted,
101    );
102
103    // Kbd chips on the right ⌘ K
104    let chip_y = origin[1] + (height - 16.0) * 0.5;
105    let mut x = origin[0] + width - 12.0;
106    for ch in ["K", "⌘"] {
107        let measured = ui.measure_text(ch, 10.5, FontFamilyHint::Monospace)[0];
108        let w = measured.max(14.0) + 10.0;
109        x -= w + 2.0;
110        paint_kbd_chip(ui, [x, chip_y], ch, theme);
111    }
112
113    let interaction = ui.interact_rect("titlebar-search", [origin[0], origin[1], width, height]);
114    (interaction, origin[0] + width)
115}
116
117/// A panel header tab. Returns whether it was clicked plus its width.
118pub fn panel_tab(
119    ui: &mut dyn UiBuilder,
120    id_salt: &str,
121    origin: [f32; 2],
122    label: &str,
123    badge: Option<&str>,
124    active: bool,
125    theme: &UiTheme,
126) -> (bool, f32) {
127    let label_w = ui.measure_text(
128        label,
129        theme.font_size_body - 1.0,
130        FontFamilyHint::Proportional,
131    )[0];
132    let badge_w = badge
133        .map(|b| ui.measure_text(b, 9.5, FontFamilyHint::Monospace)[0] + 10.0)
134        .unwrap_or(0.0);
135    let pad_x = 10.0;
136    let w = pad_x * 2.0 + label_w + if badge.is_some() { badge_w + 6.0 } else { 0.0 };
137    let h = 22.0;
138
139    let bg = if active {
140        theme.surface_active
141    } else {
142        theme.surface
143    };
144    if active {
145        ui.paint_rect_filled(origin, [w, h], bg, 4.0);
146    }
147
148    let text_color = if active { theme.text } else { theme.text_dim };
149    paint_text_size(
150        ui,
151        [
152            origin[0] + pad_x,
153            origin[1] + (h - theme.font_size_body) * 0.5,
154        ],
155        label,
156        theme.font_size_body - 1.0,
157        text_color,
158    );
159
160    if let Some(badge) = badge {
161        let badge_x = origin[0] + pad_x + label_w + 6.0;
162        let badge_y = origin[1] + 4.0;
163        ui.paint_rect_filled(
164            [badge_x, badge_y],
165            [badge_w - 4.0, 14.0],
166            if active {
167                with_alpha(theme.primary, 0.20)
168            } else {
169                theme.surface_elevated
170            },
171            999.0,
172        );
173        let badge_text_color = if active {
174            theme.primary
175        } else {
176            theme.text_dim
177        };
178        ui.paint_text_styled(
179            [badge_x + (badge_w - 4.0) * 0.5, badge_y + 1.0],
180            badge,
181            9.5,
182            badge_text_color,
183            FontFamilyHint::Monospace,
184            TextAlign::Center,
185        );
186    }
187
188    let interaction = ui.interact_rect(id_salt, [origin[0], origin[1], w, h]);
189    (interaction.clicked, w)
190}
191
192/// Paints a panel-header strip: subtle gradient bg, hairline at bottom, then
193/// a row of tabs returned through `tabs_clicked` and a row of action icons on
194/// the right. Returns the y-coordinate where panel content can begin.
195pub fn paint_panel_header(
196    ui: &mut dyn UiBuilder,
197    panel_rect: [f32; 4],
198    height: f32,
199    theme: &UiTheme,
200) {
201    let [x, y, w, _] = panel_rect;
202    super::paint::paint_vertical_gradient(
203        ui,
204        [x, y, w, height],
205        theme.surface_elevated,
206        theme.surface,
207        4,
208    );
209    ui.paint_line(
210        [x, y + height],
211        [x + w, y + height],
212        with_alpha(theme.separator, 0.55),
213        1.0,
214    );
215}