From c8a2aceab79e4235b82a71c3fed9bf4b27d2ac63 Mon Sep 17 00:00:00 2001 From: Past9 Date: Tue, 8 Nov 2022 05:49:43 -0700 Subject: [PATCH] Implement add popups (#63) * Implement add popups * Fix clippy errors --- examples/tab_add_popup.rs | 137 ++++++++++++++++++++++++++++++++++++++ src/lib.rs | 16 +++++ src/popup.rs | 104 +++++++++++++++++++++++++++++ src/style.rs | 9 +++ 4 files changed, 266 insertions(+) create mode 100644 examples/tab_add_popup.rs create mode 100644 src/popup.rs diff --git a/examples/tab_add_popup.rs b/examples/tab_add_popup.rs new file mode 100644 index 0000000..8becdb5 --- /dev/null +++ b/examples/tab_add_popup.rs @@ -0,0 +1,137 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release + +use eframe::{egui, NativeOptions}; + +use egui::{Color32, RichText}; +use egui_dock::{DockArea, NodeIndex, StyleBuilder, Tree}; + +fn main() { + let options = NativeOptions::default(); + eframe::run_native( + "My egui App", + options, + Box::new(|_cc| Box::new(MyApp::default())), + ); +} + +enum MyTabKind { + Regular, + Fancy, +} + +struct MyTab { + kind: MyTabKind, + node: NodeIndex, +} +impl MyTab { + fn regular(node_index: usize) -> Self { + Self { + kind: MyTabKind::Regular, + node: NodeIndex(node_index), + } + } + + fn fancy(node_index: usize) -> Self { + Self { + kind: MyTabKind::Fancy, + node: NodeIndex(node_index), + } + } + + fn title(&self) -> String { + match self.kind { + MyTabKind::Regular => format!("Regular Tab {}", self.node.0), + MyTabKind::Fancy => format!("Fancy Tab {}", self.node.0), + } + } + + fn content(&self) -> RichText { + match self.kind { + MyTabKind::Regular => { + RichText::new(format!("Content of {}. This tab is ho-hum.", self.title())) + } + MyTabKind::Fancy => RichText::new(format!( + "Content of {}. This tab sure is fancy!", + self.title() + )) + .italics() + .size(20.0) + .color(Color32::from_rgb(255, 128, 64)), + } + } +} + +struct TabViewer<'a> { + added_nodes: &'a mut Vec, +} + +impl egui_dock::TabViewer for TabViewer<'_> { + type Tab = MyTab; + + fn ui(&mut self, ui: &mut egui::Ui, tab: &mut Self::Tab) { + ui.label(tab.content()); + } + + fn title(&mut self, tab: &mut Self::Tab) -> egui::WidgetText { + tab.title().into() + } + + fn add_popup(&mut self, ui: &mut egui::Ui, node: NodeIndex) { + ui.set_min_width(120.0); + ui.style_mut().visuals.button_frame = false; + + if ui.button("Regular tab").clicked() { + self.added_nodes.push(MyTab::regular(node.0)); + } + + if ui.button("Fancy tab").clicked() { + self.added_nodes.push(MyTab::fancy(node.0)); + } + } +} + +struct MyApp { + tree: Tree, + counter: usize, +} + +impl Default for MyApp { + fn default() -> Self { + let mut tree = Tree::new(vec![MyTab::regular(1), MyTab::fancy(2)]); + + // You can modify the tree before constructing the dock + let [a, b] = tree.split_left(NodeIndex::root(), 0.3, vec![MyTab::fancy(3)]); + let [_, _] = tree.split_below(a, 0.7, vec![MyTab::fancy(4)]); + let [_, _] = tree.split_below(b, 0.5, vec![MyTab::regular(5)]); + + Self { tree, counter: 6 } + } +} + +impl eframe::App for MyApp { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + let mut added_nodes = Vec::new(); + DockArea::new(&mut self.tree) + .style( + StyleBuilder::from_egui(ctx.style().as_ref()) + .show_add_buttons(true) + .show_add_popup(true) + .build(), + ) + .show( + ctx, + &mut TabViewer { + added_nodes: &mut added_nodes, + }, + ); + + added_nodes.drain(..).for_each(|node| { + self.tree.set_focused_node(node.node); + self.tree.push_to_focused_leaf(MyTab { + kind: node.kind, + node: NodeIndex(self.counter), + }); + self.counter += 1; + }); + } +} diff --git a/src/lib.rs b/src/lib.rs index d820e0c..5576749 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -72,6 +72,7 @@ pub use egui; use utils::*; mod dynamic_tab; +mod popup; mod style; mod tree; mod utils; @@ -173,6 +174,12 @@ pub trait TabViewer { /// particular add button was pressed on. fn on_add(&mut self, _node: NodeIndex) {} + /// Content of add_popup. Displays a popup under the add button. Useful for selecting + /// what type of tab to add. + /// + /// This requires the dock style's `show_add_buttons` and `show_add_popup` to be `true`. + fn add_popup(&mut self, _ui: &mut Ui, _node: NodeIndex) {} + /// This is called every frame after `ui` is called (if the tab is active). /// /// Returns `true` if the tab should be forced to close, `false` otherwise. @@ -467,7 +474,16 @@ impl<'tree, Tab> DockArea<'tree, Tab> { let response = style.tab_plus(ui); let response = ui.interact(response.rect, id, Sense::click()); + let popup_id = id.with("tab_add_popup"); + + popup::popup_under_widget(ui, popup_id, &response, |ui| { + tab_viewer.add_popup(ui, node_index); + }); + if response.clicked() { + if style.show_add_popup { + ui.memory().toggle_popup(popup_id); + } tab_viewer.on_add(node_index); } }; diff --git a/src/popup.rs b/src/popup.rs new file mode 100644 index 0000000..2547c5e --- /dev/null +++ b/src/popup.rs @@ -0,0 +1,104 @@ +use egui::{Align, Area, Frame, Id, Key, Layout, NumExt, Order, Rect, Response, Ui, Vec2}; + +#[derive(Clone, Default)] +struct State { + size: Vec2, +} + +// This code was taken from the example here: https://github.com/emilk/egui/pull/1653#issuecomment-1133671051. +// It's needed because `egui`'s `popup_below_widget` currently doesn't respect window edges and will fall outside +// of them if near the right or bottom edge. It should be replaced with `egui` functions when this is fixed. +// +// All credit goes to https://github.com/zicklag. + +/// Like `egui::popup_under_widget`, but pops up to the left, so that the popup doesn't go off the screen +pub(crate) fn popup_under_widget( + ui: &Ui, + popup_id: Id, + widget_response: &Response, + add_contents: impl FnOnce(&mut Ui) -> R, +) -> Option { + if ui.memory().is_popup_open(popup_id) { + let state: Option = ui.data().get_temp(popup_id); + + // If this is the first draw, we don't know the popup size yet, so we don't know how to + // position the popup + if state.is_none() { + ui.ctx().request_repaint(); + } + + let mut state = state.unwrap_or_default(); + + let rect = Rect { + min: widget_response.rect.left_bottom(), + max: widget_response.rect.left_bottom() + state.size, + }; + let inner = Area::new(popup_id) + .order(Order::Foreground) + .fixed_pos(constrain_window_rect_to_area(ui.ctx(), rect, None).min) + .movable(true) + .show(ui.ctx(), |ui| { + // Note: we use a separate clip-rect for this area, so the popup can be outside the parent. + // See https://github.com/emilk/egui/issues/825 + let frame = Frame::popup(ui.style()); + let frame_margin = frame.inner_margin + frame.outer_margin; + let result = frame + .show(ui, |ui| { + ui.with_layout(Layout::top_down_justified(Align::LEFT), |ui| { + ui.set_width(widget_response.rect.width() - frame_margin.sum().x); + add_contents(ui) + }) + .inner + }) + .inner; + + state.size = ui.min_rect().size(); + + result + }) + .inner; + + *ui.data().get_temp_mut_or_default(popup_id) = state; + + if ui.input().key_pressed(Key::Escape) || widget_response.clicked_elsewhere() { + ui.memory().close_popup(); + } + Some(inner) + } else { + None + } +} + +/// Copied egui because it is a private function on `egui::Context` +pub(crate) fn constrain_window_rect_to_area( + ctx: &egui::Context, + window: Rect, + area: Option, +) -> Rect { + let mut area = area.unwrap_or_else(|| ctx.available_rect()); + + if window.width() > area.width() { + // Allow overlapping side bars. + // This is important for small screens, e.g. mobiles running the web demo. + area.max.x = ctx.input().screen_rect().max.x; + area.min.x = ctx.input().screen_rect().min.x; + } + if window.height() > area.height() { + // Allow overlapping top/bottom bars: + area.max.y = ctx.input().screen_rect().max.y; + area.min.y = ctx.input().screen_rect().min.y; + } + + let mut pos = window.min; + + // Constrain to screen, unless window is too large to fit: + let margin_x = (window.width() - area.width()).at_least(0.0); + let margin_y = (window.height() - area.height()).at_least(0.0); + + pos.x = pos.x.at_most(area.right() + margin_x - window.width()); // move left if needed + pos.x = pos.x.at_least(area.left() - margin_x); // move right if needed + pos.y = pos.y.at_most(area.bottom() + margin_y - window.height()); // move right if needed + pos.y = pos.y.at_least(area.top() - margin_y); // move down if needed + + Rect::from_min_size(pos, window.size()) +} diff --git a/src/style.rs b/src/style.rs index 7866683..3576ba0 100644 --- a/src/style.rs +++ b/src/style.rs @@ -45,6 +45,7 @@ pub struct Style { pub add_tab_active_color: Color32, pub add_tab_background_color: Color32, pub show_add_buttons: bool, + pub show_add_popup: bool, pub show_context_menu: bool, pub tab_include_scrollarea: bool, @@ -83,6 +84,7 @@ impl Default for Style { add_tab_active_color: Color32::WHITE, add_tab_background_color: Color32::GRAY, show_add_buttons: false, + show_add_popup: false, tabs_are_draggable: true, expand_tabs: false, @@ -551,6 +553,13 @@ impl StyleBuilder { self } + /// Shows / Hides the add button popup. + #[inline(always)] + pub fn show_add_popup(mut self, show_add_popup: bool) -> Self { + self.style.show_add_popup = show_add_popup; + self + } + /// Color of tab title when the tab is unfocused. #[inline(always)] pub fn with_tab_text_color_unfocused(mut self, tab_text_color_unfocused: Color32) -> Self {