diff --git a/gui/src/search/mod.rs b/gui/src/search/mod.rs index 349f165..8baea6c 100644 --- a/gui/src/search/mod.rs +++ b/gui/src/search/mod.rs @@ -1,4 +1,5 @@ pub mod query_window; +pub mod segmented_button; use std::{ cell::RefCell, collections::HashMap, @@ -11,12 +12,16 @@ use std::{ }; use crate::{ - ApiDocsState, LogState, TraceProvider, icon_colored, notifications::draw_x, rect, - search::query_window::PaginatedResults, spawn_task, + ApiDocsState, LogState, TraceProvider, icon_colored, + notifications::draw_x, + rect, + search::{query_window::PaginatedResults, segmented_button::SegmentedIconButtons}, + spawn_task, }; use crossbeam::channel::Receiver; use egui::{ - Color32, CornerRadius, Margin, Pos2, Rect, Response, Sense, Separator, TextEdit, Ui, pos2, vec2, + Color32, CornerRadius, Margin, Pos2, Rect, Response, Sense, Separator, TextEdit, Ui, + epaint::RectShape, pos2, vec2, }; use entrace_core::LogProviderError; use entrace_query::{ @@ -287,6 +292,7 @@ pub fn bottom_panel_ui( let total_top_padding = resize_width + text_field_margin.topf(); let search_rect = search_response.rect; let search_rect = search_rect.with_min_y(search_rect.min.y - total_top_padding); + let icon_size = 20.0; let rect_top_left = pos2(avail.max.x - (3.0 * icon_size), search_rect.min.y); let rect_bottom_right = pos2(avail.max.x, search_rect.min.y + icon_size); @@ -296,42 +302,14 @@ pub fn bottom_panel_ui( egui::Theme::Dark => Color32::DARK_GRAY, egui::Theme::Light => Color32::LIGHT_GRAY, }; - ui.painter().rect_filled(rect2, bg_corner_radius, color); - - // make sure the items we add do not overflow the bottom panel, since that will - // grow it. to do this, we define an inner rect. - let spacing = 3.0; - let inner_rect_min = rect2.min + vec2(3.0, 3.0); - let inner_rect_max = rect2.max + vec2(-1.0, -3.0); - let total_width = inner_rect_max.x - inner_rect_min.x; - // [||] - let segment_width = (total_width - (2.0 * spacing)) / 3.0; - let segment_size = vec2(segment_width, inner_rect_max.y - inner_rect_min.y); - let inner_left = Rect::from_min_size(inner_rect_min, segment_size); - let inner_mid = - Rect::from_min_size(pos2(inner_left.max.x + spacing, inner_rect_min.y), segment_size); - let inner_right = rect![pos2(inner_mid.max.x + spacing, inner_rect_min.y), inner_rect_max]; - let bg_left = rect![rect2.min, pos2(inner_left.max.x, rect2.max.y)]; - let bg_mid = rect![pos2(inner_mid.min.x, rect2.min.y), pos2(inner_mid.max.x, rect2.max.y)]; - let bg_right = rect![pos2(inner_right.min.x, rect2.min.y), rect2.max]; - - let (topy, bottomy) = (rect2.min.y + 3.0, rect2.max.y - 1.0); - let sep1_rect = - rect![pos2(inner_left.max.x + spacing, topy), pos2(inner_mid.min.x - spacing, bottomy)]; - let sep2_rect = - rect![pos2(inner_mid.max.x + spacing, topy), pos2(inner_right.min.x - spacing, bottomy)]; - ui.put(sep1_rect, Separator::default().vertical()); - ui.put(sep2_rect, Separator::default().vertical()); - fn paint_label( + fn paint_label( ui: &mut Ui, bg_rect: Rect, bg_corner_radius: CornerRadius, inner_rect: Rect, - label_callback: impl FnOnce(&mut Ui, Color32), on_click: impl FnOnce(Response), - hover_text: Option<&str>, + label_callback: impl FnOnce(&mut Ui, Color32) -> L, on_click: impl FnOnce(Response) -> O, + hover_text: &str, ) { let mut resp = ui.allocate_rect(inner_rect, Sense::click()); - if let Some(x) = hover_text { - resp = resp.on_hover_text(x); - } + resp = resp.on_hover_text(hover_text); if resp.hovered() { ui.painter().rect_filled(bg_rect, bg_corner_radius, Color32::GRAY.gamma_multiply(0.5)); } @@ -342,46 +320,46 @@ pub fn bottom_panel_ui( on_click(resp); } } - paint_label( - ui, - bg_left, - bg_corner_radius, - inner_left, - |ui, color| { - ui.put(inner_left, icon_colored!("../../vendor/icons/play_arrow.svg", color)); - }, - |_| search_state.new_query(log_state.trace_provider.clone()), - Some("Run (Ctrl+Enter)"), - ); - paint_label( - ui, - bg_mid, - bg_corner_radius, - inner_mid, - |ui, color| { - ui.put(inner_mid, icon_colored!("../../vendor/icons/docs.svg", color)); - }, - |_| { - api_docs_state.open = true; - }, - Some("Lua API Docs"), - ); - - paint_label( - ui, - bg_right, - CornerRadius::ZERO, - inner_right, - |ui, color| { - ui.put(inner_right, icon_colored!("../../vendor/icons/settings.svg", color)); - }, - |_| { - info!(settings_btn_rect = ?inner_right, "Query settings icon clicked"); - search_state.settings.data = - QuerySettingsDialogData::Open { settings_button_rect: inner_right, position: None } - }, - Some("Settings"), - ); + let inner_to_bg_rect = + |inner: Rect| rect![pos2(inner.min.x, rect2.min.y), pos2(inner.max.x, rect2.max.y)]; + SegmentedIconButtons::new(RectShape::filled(rect2, bg_corner_radius, color)) + .separator_y_padding([3.0, 1.0]) + .with_contents(|ui, rects: [Rect; 3]| { + paint_label( + ui, + inner_to_bg_rect(rects[0]).with_min_x(rect2.min.x), + bg_corner_radius, + rects[0], + |ui, clr| ui.put(rects[0], icon_colored!("../../vendor/icons/play_arrow.svg", clr)), + |_| search_state.new_query(log_state.trace_provider.clone()), + "Run (Ctrl+Enter)", + ); + paint_label( + ui, + inner_to_bg_rect(rects[1]), + CornerRadius::ZERO, + rects[1], + |ui, clr| ui.put(rects[1], icon_colored!("../../vendor/icons/docs.svg", clr)), + |_| api_docs_state.open = true, + "Lua API Docs", + ); + paint_label( + ui, + inner_to_bg_rect(rect![rects[2].min, rect2.max]), + CornerRadius::ZERO, + rects[2], + |ui, clr| ui.put(rects[2], icon_colored!("../../vendor/icons/settings.svg", clr)), + |_| { + info!(settings_btn_rect = ?rects[2], "Query settings icon clicked"); + search_state.settings.data = QuerySettingsDialogData::Open { + settings_button_rect: rects[2], + position: None, + } + }, + "Settings", + ); + }) + .show(ui); } pub struct LocatingStarted { pub target: u32, diff --git a/gui/src/search/segmented_button.rs b/gui/src/search/segmented_button.rs new file mode 100644 index 0000000..1562456 --- /dev/null +++ b/gui/src/search/segmented_button.rs @@ -0,0 +1,123 @@ +use crate::rect; +use egui::{Color32, Rect, Sense, Separator, Ui, Vec2, epaint::RectShape, pos2, vec2}; + +pub struct SegmentedIconButtons { + shape: RectShape, + /// controls the spacing between the segments inside the container. 3.0 by default. + inner_spacing: f32, + /// the area for the inner rect. by default rect![shape.rect.min + spacing2, shape.rect.max - spacing2] + inner_rect: Rect, + add_contents: F, + separator_padding_y: [f32; 2], +} + +impl SegmentedIconButtons { + pub fn new(shape: RectShape) -> Self { + let rect = shape.rect; + let spacing2 = Vec2::splat(3.0); + let inner_rect = rect![rect.min + spacing2, rect.max - spacing2]; + Self { + shape, + inner_spacing: 3.0, + add_contents: (), + inner_rect, + separator_padding_y: [0.0; 2], + } + } +} + +impl SegmentedIconButtons { + pub fn inner_spacing(mut self, spacing: f32) -> Self { + self.inner_spacing = spacing; + self + } + pub fn inner_rect(mut self, inner_rect: Rect) -> Self { + self.inner_rect = inner_rect; + self + } + + pub fn with_contents(self, add_contents: G) -> SegmentedIconButtons + where + G: FnOnce(&mut Ui, [Rect; N]) -> R, + { + SegmentedIconButtons { + shape: self.shape, + inner_spacing: self.inner_spacing, + add_contents, + inner_rect: self.inner_rect, + separator_padding_y: self.separator_padding_y, + } + } + + pub fn separator_y_padding(mut self, separator_y_padding: [f32; 2]) -> Self { + self.separator_padding_y = separator_y_padding; + self + } +} + +impl SegmentedIconButtons +where + F: FnOnce(&mut Ui, [Rect; N]) -> R, +{ + pub fn show(self, ui: &mut Ui) { + if N == 0 { + return; + } + + let rect = self.shape.rect; + ui.painter().add(self.shape); + let mut rects = [Rect::NOTHING; N]; + + let total_width = self.inner_rect.width(); + let segment_width = + (total_width - ((N - 1) as f32 * self.inner_spacing)) / (N as f32).max(1.0); + let segment_size = vec2(segment_width, self.inner_rect.height()); + + let (topy, bottomy, inner) = ( + rect.min.y + self.separator_padding_y[0], + rect.max.y - self.separator_padding_y[1], + &self.inner_rect, + ); + let mut last_x = self.inner_rect.min.x; + for (i, rect) in rects.iter_mut().enumerate() { + *rect = if i == N - 1 { + rect![pos2(last_x, inner.min.y), inner.max] + } else { + Rect::from_min_size(pos2(last_x, inner.min.y), segment_size) + }; + + if i > 0 { + let sep_rect = + rect![pos2(last_x - self.inner_spacing, topy), pos2(last_x, bottomy)]; + ui.put(sep_rect, Separator::default().vertical()); + } + last_x = rect.max.x + self.inner_spacing; + } + + let _inner = (self.add_contents)(ui, rects); + } +} + +pub fn demo(ui: &mut Ui) { + let (rect, _) = ui.allocate_at_least(vec2(150.0, 24.0), Sense::hover()); + let mut shape = RectShape::filled(rect, 4.0, ui.visuals().widgets.inactive.bg_fill); + shape.stroke = ui.style().visuals.widgets.inactive.bg_stroke; + + SegmentedIconButtons::new(shape.clone()) + .inner_rect(shape.rect) + .with_contents(|ui, rects: [Rect; 5]| { + for (i, r) in rects.into_iter().enumerate() { + let label = (i + 1).to_string(); + let resp = ui.put(r, egui::Button::new(label).frame(false)); + + if resp.hovered() { + ui.painter().rect_filled(r, 0.0, Color32::GRAY.gamma_multiply_u8(24)); + } + + if resp.clicked() { + println!("Clicked segment {}", i + 1); + } + } + }) + .show(ui); +}