Krolyxon
Tag v0.3.0

Branches

Tags

Refs →

bench51cratesbench51-guisrcwidgets.rs

378 lines · 11.6 KB · Rust

1//! Nothing-style controls: buttons, segmented controls and bars, bit chips,
2//! pin dots, stat rows and empty states.
3
4use eframe::egui::{
5 self, Align, Align2, Color32, CornerRadius, CursorIcon, FontId, Layout, Painter, Rect,
6 Response, RichText, Sense, Stroke, StrokeKind, TextWrapMode, Ui, WidgetText, pos2, vec2,
7};
8
9use crate::icons::{self, Icon};
10use crate::theme::{self, ACCENT, LABEL, LABEL_TRACKING, palette};
11
12fn galley(ui: &Ui, text: RichText) -> std::sync::Arc<egui::Galley> {
13 WidgetText::from(text).into_galley(
14 ui,
15 Some(TextWrapMode::Extend),
16 f32::INFINITY,
17 egui::TextStyle::Small,
18 )
19}
20
21/// Square toolbar button with a monoline icon. `on` shows it latched
22/// (inverted), e.g. while the debug session is active.
23pub fn icon_button(ui: &mut Ui, icon: Icon, tip: &str, enabled: bool, on: bool) -> Response {
24 let p = palette(ui.ctx());
25 let size = vec2(30.0, 28.0);
26 let sense = if enabled {
27 Sense::click()
28 } else {
29 Sense::hover()
30 };
31 let (rect, resp) = ui.allocate_exact_size(size, sense);
32 let resp = resp.on_hover_text(tip);
33 if ui.is_rect_visible(rect) {
34 let painter = ui.painter();
35 let radius = CornerRadius::same(4);
36 let fg = if !enabled {
37 p.text_disabled
38 } else if on {
39 p.black
40 } else if resp.hovered() {
41 p.text_display
42 } else {
43 p.text_primary
44 };
45 if on && enabled {
46 painter.rect_filled(rect, radius, p.text_display);
47 } else if resp.hovered() {
48 painter.rect_stroke(
49 rect,
50 radius,
51 Stroke::new(1.0, p.border_visible),
52 StrokeKind::Inside,
53 );
54 }
55 icons::paint(painter, icons::icon_rect(rect, 16.0), icon, fg);
56 }
57 if enabled {
58 resp.on_hover_cursor(CursorIcon::PointingHand)
59 } else {
60 resp
61 }
62}
63
64#[derive(Clone, Copy, PartialEq, Eq)]
65pub enum PillKind {
66 /// White pill with black text: the one action to take.
67 Primary,
68 /// Outlined pill.
69 Secondary,
70 /// Text only.
71 Ghost,
72}
73
74/// Pill button: Space Mono, ALL CAPS, tracked.
75pub fn pill(ui: &mut Ui, text: &str, kind: PillKind) -> Response {
76 let p = palette(ui.ctx());
77 let label = RichText::new(text.to_uppercase())
78 .font(FontId::monospace(12.0))
79 .extra_letter_spacing(0.7);
80 let g = galley(ui, label);
81 let size = vec2(g.size().x + 36.0, 32.0);
82 let (rect, resp) = ui.allocate_exact_size(size, Sense::click());
83 if ui.is_rect_visible(rect) {
84 let painter = ui.painter();
85 let hovered = resp.hovered();
86 let (fill, stroke, fg) = match kind {
87 PillKind::Primary => (p.text_display, Stroke::NONE, p.black),
88 PillKind::Secondary => (
89 Color32::TRANSPARENT,
90 Stroke::new(
91 1.0,
92 if hovered {
93 p.text_primary
94 } else {
95 p.border_visible
96 },
97 ),
98 p.text_primary,
99 ),
100 PillKind::Ghost => (
101 Color32::TRANSPARENT,
102 Stroke::NONE,
103 if hovered {
104 p.text_display
105 } else {
106 p.text_secondary
107 },
108 ),
109 };
110 painter.rect(
111 rect,
112 CornerRadius::same(255),
113 fill,
114 stroke,
115 StrokeKind::Inside,
116 );
117 let pos = Align2::CENTER_CENTER
118 .align_size_within_rect(g.size(), rect)
119 .min;
120 painter.galley(pos, g, fg);
121 }
122 resp.on_hover_cursor(CursorIcon::PointingHand)
123}
124
125/// Segmented control: the active segment is inverted. Returns true if the
126/// value changed.
127pub fn segmented<T: PartialEq + Copy>(ui: &mut Ui, value: &mut T, options: &[(T, &str)]) -> bool {
128 let p = palette(ui.ctx());
129 let galleys: Vec<_> = options
130 .iter()
131 .map(|(_, l)| galley(ui, p.label(l)))
132 .collect();
133 let pad = 10.0;
134 let widths: Vec<f32> = galleys.iter().map(|g| g.size().x + 2.0 * pad).collect();
135 let size = vec2(widths.iter().sum(), 24.0);
136 let (rect, _) = ui.allocate_exact_size(size, Sense::hover());
137 let radius = CornerRadius::same(4);
138 let mut changed = false;
139 let mut x = rect.left();
140 for (i, ((v, label), g)) in options.iter().zip(galleys).enumerate() {
141 let seg = Rect::from_min_size(pos2(x, rect.top()), vec2(widths[i], rect.height()));
142 x += widths[i];
143 let resp = ui
144 .interact(seg, ui.auto_id_with(("seg", label, i)), Sense::click())
145 .on_hover_cursor(CursorIcon::PointingHand);
146 let active = *value == *v;
147 if resp.clicked() && !active {
148 *value = *v;
149 changed = true;
150 }
151 let fg = if active {
152 ui.painter().rect_filled(seg, radius, p.text_display);
153 p.black
154 } else if resp.hovered() {
155 p.text_display
156 } else {
157 p.text_secondary
158 };
159 let pos = Align2::CENTER_CENTER
160 .align_size_within_rect(g.size(), seg)
161 .min;
162 ui.painter().galley(pos, g, fg);
163 }
164 ui.painter().rect_stroke(
165 rect,
166 radius,
167 Stroke::new(1.0, p.border_visible),
168 StrokeKind::Inside,
169 );
170 changed
171}
172
173/// Segmented bar: `filled` of `segments` square blocks with 2 px gaps.
174/// Returns the index of a clicked segment.
175pub fn segment_bar(
176 ui: &mut Ui,
177 width: f32,
178 height: f32,
179 segments: usize,
180 filled: usize,
181 color: Color32,
182 clickable: bool,
183) -> Option<usize> {
184 let p = palette(ui.ctx());
185 let sense = if clickable {
186 Sense::click()
187 } else {
188 Sense::hover()
189 };
190 let (rect, resp) = ui.allocate_exact_size(vec2(width, height), sense);
191 let gap = 2.0;
192 let seg_w = (width - gap * (segments as f32 - 1.0)) / segments as f32;
193 let hover_idx = resp
194 .hover_pos()
195 .filter(|_| clickable)
196 .map(|pos| (((pos.x - rect.left()) / (seg_w + gap)) as usize).min(segments - 1));
197 let empty = if ui.visuals().dark_mode {
198 p.border
199 } else {
200 Color32::from_gray(0xE0)
201 };
202 for i in 0..segments {
203 let x = rect.left() + i as f32 * (seg_w + gap);
204 let r = Rect::from_min_size(pos2(x, rect.top()), vec2(seg_w, height));
205 let c = if i < filled {
206 color
207 } else if hover_idx.is_some_and(|h| i <= h) {
208 p.text_disabled
209 } else {
210 empty
211 };
212 ui.painter().rect_filled(r, 0.0, c);
213 }
214 if clickable && resp.clicked() {
215 resp.on_hover_cursor(CursorIcon::PointingHand);
216 return hover_idx;
217 }
218 if clickable {
219 resp.on_hover_cursor(CursorIcon::PointingHand);
220 }
221 None
222}
223
224/// Bit flag as a technical tag: lit when set. Clicking toggles it.
225pub fn bit_chip(ui: &mut Ui, name: &str, on: bool) -> Response {
226 let p = palette(ui.ctx());
227 let g = galley(
228 ui,
229 RichText::new(name)
230 .font(FontId::monospace(LABEL))
231 .extra_letter_spacing(LABEL_TRACKING * 0.5),
232 );
233 let size = vec2(g.size().x + 12.0, 20.0);
234 let (rect, resp) = ui.allocate_exact_size(size, Sense::click());
235 if ui.is_rect_visible(rect) {
236 let hovered = resp.hovered();
237 let (stroke, fg) = if on {
238 (p.text_display, p.text_display)
239 } else if hovered {
240 (p.text_secondary, p.text_secondary)
241 } else {
242 (p.border_visible, p.text_disabled)
243 };
244 ui.painter().rect_stroke(
245 rect,
246 CornerRadius::same(4),
247 Stroke::new(1.0, stroke),
248 StrokeKind::Inside,
249 );
250 let pos = Align2::CENTER_CENTER
251 .align_size_within_rect(g.size(), rect)
252 .min;
253 ui.painter().galley(pos, g, fg);
254 }
255 resp.on_hover_cursor(CursorIcon::PointingHand)
256}
257
258#[derive(Clone, Copy, PartialEq, Eq)]
259pub enum Dot {
260 High,
261 Low,
262 /// Held low from outside the chip, like a pressed button.
263 PulledLow,
264}
265
266/// A port pin as an LED-like dot.
267pub fn pin_dot(ui: &mut Ui, state: Dot) -> Response {
268 let p = palette(ui.ctx());
269 let (rect, resp) = ui.allocate_exact_size(vec2(18.0, 18.0), Sense::click());
270 if ui.is_rect_visible(rect) {
271 let c = rect.center();
272 let r = 5.0;
273 match state {
274 Dot::High => {
275 ui.painter().circle_filled(c, r, p.text_display);
276 }
277 Dot::PulledLow => {
278 ui.painter().circle_filled(c, r, ACCENT);
279 }
280 Dot::Low => {
281 ui.painter()
282 .circle_stroke(c, r - 0.5, Stroke::new(1.5, p.text_disabled));
283 }
284 }
285 if resp.hovered() {
286 ui.painter()
287 .circle_stroke(c, r + 3.0, Stroke::new(1.0, p.border_visible));
288 }
289 }
290 resp.on_hover_cursor(CursorIcon::PointingHand)
291}
292
293/// Label on the left, value on the right of the available width.
294pub fn stat_row(ui: &mut Ui, label: &str, value: RichText) {
295 let p = palette(ui.ctx());
296 ui.horizontal(|ui| {
297 ui.label(p.label(label));
298 ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
299 ui.label(value);
300 });
301 });
302}
303
304/// Uniform dot grid, the dot-matrix motif for empty backgrounds.
305pub fn dot_grid(painter: &Painter, rect: Rect, color: Color32, pitch: f32) {
306 let x0 = (rect.left() / pitch).ceil() * pitch;
307 let y0 = (rect.top() / pitch).ceil() * pitch;
308 let mut y = y0;
309 while y < rect.bottom() {
310 let mut x = x0;
311 while x < rect.right() {
312 painter.circle_filled(pos2(x, y), 0.9, color);
313 x += pitch;
314 }
315 y += pitch;
316 }
317}
318
319/// Centred two-line empty state on a faint dot grid.
320pub fn empty_state(ui: &mut Ui, headline: &str, hint: &str) {
321 let p = palette(ui.ctx());
322 let rect = ui.available_rect_before_wrap();
323 let grid = if ui.visuals().dark_mode {
324 p.border
325 } else {
326 p.border_visible.gamma_multiply(0.6)
327 };
328 dot_grid(ui.painter(), rect, grid, 16.0);
329 ui.scope_builder(
330 egui::UiBuilder::new()
331 .max_rect(rect)
332 .layout(Layout::top_down(Align::Center)),
333 |ui| {
334 ui.add_space((rect.height() / 2.0 - 24.0).max(8.0));
335 ui.label(p.label(headline).color(p.text_secondary));
336 ui.add_space(6.0);
337 ui.label(
338 RichText::new(hint)
339 .size(theme::BODY - 1.0)
340 .color(p.text_disabled),
341 );
342 },
343 );
344}
345
346/// ALL CAPS section heading with breathing room above.
347pub fn section(ui: &mut Ui, title: &str) {
348 let p = palette(ui.ctx());
349 ui.add_space(12.0);
350 ui.label(p.label(title).color(p.text_disabled));
351 ui.add_space(2.0);
352}
353
354/// Underlined single-line input in Space Mono.
355pub fn underline_input(ui: &mut Ui, text: &mut String, hint: &str, width: f32) -> Response {
356 let p = palette(ui.ctx());
357 let out = egui::TextEdit::singleline(text)
358 .font(FontId::monospace(13.0))
359 .hint_text(
360 RichText::new(hint)
361 .font(FontId::monospace(13.0))
362 .color(p.text_disabled),
363 )
364 .frame(egui::Frame::NONE)
365 .margin(egui::Margin::symmetric(0, 3))
366 .desired_width(width)
367 .show(ui);
368 let resp = out.response.response;
369 let color = if resp.has_focus() {
370 p.text_primary
371 } else {
372 p.border_visible
373 };
374 let r = resp.rect;
375 ui.painter()
376 .hline(r.x_range(), r.bottom(), Stroke::new(1.0, color));
377 resp
378}