Skip to content

Commit 8b72bf4

Browse files
committed
gui: new segmented button
1 parent 9d70b7c commit 8b72bf4

File tree

2 files changed

+176
-75
lines changed

2 files changed

+176
-75
lines changed

gui/src/search/mod.rs

Lines changed: 53 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
pub mod query_window;
2+
pub mod segmented_button;
23
use std::{
34
cell::RefCell,
45
collections::HashMap,
@@ -11,12 +12,16 @@ use std::{
1112
};
1213

1314
use crate::{
14-
ApiDocsState, LogState, TraceProvider, icon_colored, notifications::draw_x, rect,
15-
search::query_window::PaginatedResults, spawn_task,
15+
ApiDocsState, LogState, TraceProvider, icon_colored,
16+
notifications::draw_x,
17+
rect,
18+
search::{query_window::PaginatedResults, segmented_button::SegmentedIconButtons},
19+
spawn_task,
1620
};
1721
use crossbeam::channel::Receiver;
1822
use egui::{
19-
Color32, CornerRadius, Margin, Pos2, Rect, Response, Sense, Separator, TextEdit, Ui, pos2, vec2,
23+
Color32, CornerRadius, Margin, Pos2, Rect, Response, Sense, Separator, TextEdit, Ui,
24+
epaint::RectShape, pos2, vec2,
2025
};
2126
use entrace_core::LogProviderError;
2227
use entrace_query::{
@@ -287,6 +292,7 @@ pub fn bottom_panel_ui(
287292
let total_top_padding = resize_width + text_field_margin.topf();
288293
let search_rect = search_response.rect;
289294
let search_rect = search_rect.with_min_y(search_rect.min.y - total_top_padding);
295+
290296
let icon_size = 20.0;
291297
let rect_top_left = pos2(avail.max.x - (3.0 * icon_size), search_rect.min.y);
292298
let rect_bottom_right = pos2(avail.max.x, search_rect.min.y + icon_size);
@@ -296,42 +302,14 @@ pub fn bottom_panel_ui(
296302
egui::Theme::Dark => Color32::DARK_GRAY,
297303
egui::Theme::Light => Color32::LIGHT_GRAY,
298304
};
299-
ui.painter().rect_filled(rect2, bg_corner_radius, color);
300-
301-
// make sure the items we add do not overflow the bottom panel, since that will
302-
// grow it. to do this, we define an inner rect.
303-
let spacing = 3.0;
304-
let inner_rect_min = rect2.min + vec2(3.0, 3.0);
305-
let inner_rect_max = rect2.max + vec2(-1.0, -3.0);
306-
let total_width = inner_rect_max.x - inner_rect_min.x;
307-
// [<left><spacing>|<spacing><mid><spacing>|<spacing><right>]
308-
let segment_width = (total_width - (2.0 * spacing)) / 3.0;
309-
let segment_size = vec2(segment_width, inner_rect_max.y - inner_rect_min.y);
310-
let inner_left = Rect::from_min_size(inner_rect_min, segment_size);
311-
let inner_mid =
312-
Rect::from_min_size(pos2(inner_left.max.x + spacing, inner_rect_min.y), segment_size);
313-
let inner_right = rect![pos2(inner_mid.max.x + spacing, inner_rect_min.y), inner_rect_max];
314-
let bg_left = rect![rect2.min, pos2(inner_left.max.x, rect2.max.y)];
315-
let bg_mid = rect![pos2(inner_mid.min.x, rect2.min.y), pos2(inner_mid.max.x, rect2.max.y)];
316-
let bg_right = rect![pos2(inner_right.min.x, rect2.min.y), rect2.max];
317-
318-
let (topy, bottomy) = (rect2.min.y + 3.0, rect2.max.y - 1.0);
319-
let sep1_rect =
320-
rect![pos2(inner_left.max.x + spacing, topy), pos2(inner_mid.min.x - spacing, bottomy)];
321-
let sep2_rect =
322-
rect![pos2(inner_mid.max.x + spacing, topy), pos2(inner_right.min.x - spacing, bottomy)];
323-
ui.put(sep1_rect, Separator::default().vertical());
324-
ui.put(sep2_rect, Separator::default().vertical());
325305

326-
fn paint_label(
306+
fn paint_label<L, O>(
327307
ui: &mut Ui, bg_rect: Rect, bg_corner_radius: CornerRadius, inner_rect: Rect,
328-
label_callback: impl FnOnce(&mut Ui, Color32), on_click: impl FnOnce(Response),
329-
hover_text: Option<&str>,
308+
label_callback: impl FnOnce(&mut Ui, Color32) -> L, on_click: impl FnOnce(Response) -> O,
309+
hover_text: &str,
330310
) {
331311
let mut resp = ui.allocate_rect(inner_rect, Sense::click());
332-
if let Some(x) = hover_text {
333-
resp = resp.on_hover_text(x);
334-
}
312+
resp = resp.on_hover_text(hover_text);
335313
if resp.hovered() {
336314
ui.painter().rect_filled(bg_rect, bg_corner_radius, Color32::GRAY.gamma_multiply(0.5));
337315
}
@@ -342,46 +320,46 @@ pub fn bottom_panel_ui(
342320
on_click(resp);
343321
}
344322
}
345-
paint_label(
346-
ui,
347-
bg_left,
348-
bg_corner_radius,
349-
inner_left,
350-
|ui, color| {
351-
ui.put(inner_left, icon_colored!("../../vendor/icons/play_arrow.svg", color));
352-
},
353-
|_| search_state.new_query(log_state.trace_provider.clone()),
354-
Some("Run (Ctrl+Enter)"),
355-
);
356-
paint_label(
357-
ui,
358-
bg_mid,
359-
bg_corner_radius,
360-
inner_mid,
361-
|ui, color| {
362-
ui.put(inner_mid, icon_colored!("../../vendor/icons/docs.svg", color));
363-
},
364-
|_| {
365-
api_docs_state.open = true;
366-
},
367-
Some("Lua API Docs"),
368-
);
369-
370-
paint_label(
371-
ui,
372-
bg_right,
373-
CornerRadius::ZERO,
374-
inner_right,
375-
|ui, color| {
376-
ui.put(inner_right, icon_colored!("../../vendor/icons/settings.svg", color));
377-
},
378-
|_| {
379-
info!(settings_btn_rect = ?inner_right, "Query settings icon clicked");
380-
search_state.settings.data =
381-
QuerySettingsDialogData::Open { settings_button_rect: inner_right, position: None }
382-
},
383-
Some("Settings"),
384-
);
323+
let inner_to_bg_rect =
324+
|inner: Rect| rect![pos2(inner.min.x, rect2.min.y), pos2(inner.max.x, rect2.max.y)];
325+
SegmentedIconButtons::new(RectShape::filled(rect2, bg_corner_radius, color))
326+
.separator_y_padding([3.0, 1.0])
327+
.with_contents(|ui, rects: [Rect; 3]| {
328+
paint_label(
329+
ui,
330+
inner_to_bg_rect(rects[0]).with_min_x(rect2.min.x),
331+
bg_corner_radius,
332+
rects[0],
333+
|ui, clr| ui.put(rects[0], icon_colored!("../../vendor/icons/play_arrow.svg", clr)),
334+
|_| search_state.new_query(log_state.trace_provider.clone()),
335+
"Run (Ctrl+Enter)",
336+
);
337+
paint_label(
338+
ui,
339+
inner_to_bg_rect(rects[1]),
340+
CornerRadius::ZERO,
341+
rects[1],
342+
|ui, clr| ui.put(rects[1], icon_colored!("../../vendor/icons/docs.svg", clr)),
343+
|_| api_docs_state.open = true,
344+
"Lua API Docs",
345+
);
346+
paint_label(
347+
ui,
348+
inner_to_bg_rect(rect![rects[2].min, rect2.max]),
349+
CornerRadius::ZERO,
350+
rects[2],
351+
|ui, clr| ui.put(rects[2], icon_colored!("../../vendor/icons/settings.svg", clr)),
352+
|_| {
353+
info!(settings_btn_rect = ?rects[2], "Query settings icon clicked");
354+
search_state.settings.data = QuerySettingsDialogData::Open {
355+
settings_button_rect: rects[2],
356+
position: None,
357+
}
358+
},
359+
"Settings",
360+
);
361+
})
362+
.show(ui);
385363
}
386364
pub struct LocatingStarted {
387365
pub target: u32,

gui/src/search/segmented_button.rs

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
use crate::rect;
2+
use egui::{Color32, Rect, Sense, Separator, Ui, Vec2, epaint::RectShape, pos2, vec2};
3+
4+
pub struct SegmentedIconButtons<const N: usize, F = ()> {
5+
shape: RectShape,
6+
/// controls the spacing between the segments inside the container. 3.0 by default.
7+
inner_spacing: f32,
8+
/// the area for the inner rect. by default rect![shape.rect.min + spacing2, shape.rect.max - spacing2]
9+
inner_rect: Rect,
10+
add_contents: F,
11+
separator_padding_y: [f32; 2],
12+
}
13+
14+
impl<const N: usize> SegmentedIconButtons<N, ()> {
15+
pub fn new(shape: RectShape) -> Self {
16+
let rect = shape.rect;
17+
let spacing2 = Vec2::splat(3.0);
18+
let inner_rect = rect![rect.min + spacing2, rect.max - spacing2];
19+
Self {
20+
shape,
21+
inner_spacing: 3.0,
22+
add_contents: (),
23+
inner_rect,
24+
separator_padding_y: [0.0; 2],
25+
}
26+
}
27+
}
28+
29+
impl<const N: usize, F> SegmentedIconButtons<N, F> {
30+
pub fn inner_spacing(mut self, spacing: f32) -> Self {
31+
self.inner_spacing = spacing;
32+
self
33+
}
34+
pub fn inner_rect(mut self, inner_rect: Rect) -> Self {
35+
self.inner_rect = inner_rect;
36+
self
37+
}
38+
39+
pub fn with_contents<R, G>(self, add_contents: G) -> SegmentedIconButtons<N, G>
40+
where
41+
G: FnOnce(&mut Ui, [Rect; N]) -> R,
42+
{
43+
SegmentedIconButtons {
44+
shape: self.shape,
45+
inner_spacing: self.inner_spacing,
46+
add_contents,
47+
inner_rect: self.inner_rect,
48+
separator_padding_y: self.separator_padding_y,
49+
}
50+
}
51+
52+
pub fn separator_y_padding(mut self, separator_y_padding: [f32; 2]) -> Self {
53+
self.separator_padding_y = separator_y_padding;
54+
self
55+
}
56+
}
57+
58+
impl<const N: usize, R, F> SegmentedIconButtons<N, F>
59+
where
60+
F: FnOnce(&mut Ui, [Rect; N]) -> R,
61+
{
62+
pub fn show(self, ui: &mut Ui) {
63+
if N == 0 {
64+
return;
65+
}
66+
67+
let rect = self.shape.rect;
68+
ui.painter().add(self.shape);
69+
let mut rects = [Rect::NOTHING; N];
70+
71+
let total_width = self.inner_rect.width();
72+
let segment_width =
73+
(total_width - ((N - 1) as f32 * self.inner_spacing)) / (N as f32).max(1.0);
74+
let segment_size = vec2(segment_width, self.inner_rect.height());
75+
76+
let (topy, bottomy, inner) = (
77+
rect.min.y + self.separator_padding_y[0],
78+
rect.max.y - self.separator_padding_y[1],
79+
&self.inner_rect,
80+
);
81+
let mut last_x = self.inner_rect.min.x;
82+
for (i, rect) in rects.iter_mut().enumerate() {
83+
*rect = if i == N - 1 {
84+
rect![pos2(last_x, inner.min.y), inner.max]
85+
} else {
86+
Rect::from_min_size(pos2(last_x, inner.min.y), segment_size)
87+
};
88+
89+
if i > 0 {
90+
let sep_rect =
91+
rect![pos2(last_x - self.inner_spacing, topy), pos2(last_x, bottomy)];
92+
ui.put(sep_rect, Separator::default().vertical());
93+
}
94+
last_x = rect.max.x + self.inner_spacing;
95+
}
96+
97+
let _inner = (self.add_contents)(ui, rects);
98+
}
99+
}
100+
101+
pub fn demo(ui: &mut Ui) {
102+
let (rect, _) = ui.allocate_at_least(vec2(150.0, 24.0), Sense::hover());
103+
let mut shape = RectShape::filled(rect, 4.0, ui.visuals().widgets.inactive.bg_fill);
104+
shape.stroke = ui.style().visuals.widgets.inactive.bg_stroke;
105+
106+
SegmentedIconButtons::new(shape.clone())
107+
.inner_rect(shape.rect)
108+
.with_contents(|ui, rects: [Rect; 5]| {
109+
for (i, r) in rects.into_iter().enumerate() {
110+
let label = (i + 1).to_string();
111+
let resp = ui.put(r, egui::Button::new(label).frame(false));
112+
113+
if resp.hovered() {
114+
ui.painter().rect_filled(r, 0.0, Color32::GRAY.gamma_multiply_u8(24));
115+
}
116+
117+
if resp.clicked() {
118+
println!("Clicked segment {}", i + 1);
119+
}
120+
}
121+
})
122+
.show(ui);
123+
}

0 commit comments

Comments
 (0)