Last active
August 19, 2025 08:22
-
-
Save Omustardo/6bf42b80123dd07cd0375cc3fdd26286 to your computer and use it in GitHub Desktop.
egui_dock save and load DockState from a menu
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Demo of snapshotting and loading DockState. | |
| // | |
| // The general idea is to allow a user to save their layout and then be able to reload it later. | |
| // For example, in an IDE it might be helpful to have one layout for UI programming (e.g. a render | |
| // window on one side) while more space for terminal output might be preferable in other situations. | |
| // | |
| // Being able to save and load DockState allows quickly swapping between layouts. It also allows | |
| // the program to provide preset layouts that users could choose between. | |
| use eframe::NativeOptions; | |
| use egui::containers::menu::MenuConfig; | |
| use egui::{ | |
| CentralPanel, PopupCloseBehavior, TopBottomPanel, Ui, ViewportBuilder, Widget, WidgetText, vec2, | |
| }; | |
| use egui_dock::tab_viewer::OnCloseResponse; | |
| use egui_dock::{DockArea, DockState, NodeIndex, SurfaceIndex, TabViewer}; | |
| use serde::{Deserialize, Serialize}; | |
| use std::collections::HashMap; | |
| use strum::IntoEnumIterator; | |
| use strum_macros::EnumIter; | |
| #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, EnumIter)] | |
| enum TabName { | |
| Tab1, | |
| Tab2, | |
| Tab3, | |
| Tab4, | |
| } | |
| impl TabName { | |
| fn display_name(&self) -> &'static str { | |
| match self { | |
| TabName::Tab1 => "Tab 1", | |
| TabName::Tab2 => "Tab 2", | |
| TabName::Tab3 => "Tab 3", | |
| TabName::Tab4 => "Tab 4", | |
| } | |
| } | |
| } | |
| #[derive(Debug, Clone, Serialize, Deserialize)] | |
| struct SavedLayout { | |
| dock_state: DockState<TabName>, | |
| locked: bool, | |
| } | |
| #[derive(Debug, Clone)] | |
| enum UiCommand { | |
| /// Save the current layout using the given name. If there is an existing saved layout with the | |
| /// same name, the new layout will overwrite it. The "Default" layout cannot be modified. | |
| SaveLayout { name: String }, | |
| /// Load a saved layout that has the given name. The current layout will be overwritten, | |
| /// so be sure to save it first if desired. | |
| /// If None is provided, the hard-coded default layout will be loaded. | |
| LoadLayout { name: Option<String> }, | |
| /// Delete a saved layout with the given name. If the layout does not exist, this does nothing. | |
| /// If the layout is locked, this does nothing. | |
| DeleteLayout { name: String }, | |
| /// Export a layout with the given name to the user's clipboard. | |
| ExportLayout { name: String }, | |
| /// Import a layout from the provided buffer. | |
| ImportLayout { data: String }, | |
| /// Add a new tab to the specified location in the dock layout. | |
| /// If the location doesn't exist when this command is processed, this does nothing. | |
| AddTab { | |
| tab: TabName, | |
| surface: SurfaceIndex, | |
| node: NodeIndex, | |
| }, | |
| /// Add a new tab to the parent tab's location. If the parent tab is not open, this does nothing. | |
| /// If multiple instances of the parent tab are open, the first one is used. | |
| AddTabOnParent { | |
| new_tab: TabName, | |
| parent_tab: TabName, | |
| }, | |
| /// Set the protection status of a saved layout to prevent or allow deletion. | |
| /// If the layout does not exist, this does nothing. | |
| SetLayoutLock { name: String, locked: bool }, | |
| } | |
| /// Data to show in the UI that doesn't need to be saved to disk on program exit. | |
| #[derive(Debug, Clone, Default)] | |
| struct SessionState { | |
| layout_name_input: String, | |
| import_layout_buffer: String, | |
| import_error_message: Option<String>, | |
| import_error_timer: Option<std::time::Instant>, | |
| export_success_message: Option<String>, | |
| export_success_timer: Option<std::time::Instant>, | |
| save_error_message: Option<String>, | |
| save_error_timer: Option<std::time::Instant>, | |
| } | |
| #[derive(Debug, Clone, Serialize, Deserialize)] | |
| struct MyAppState { | |
| current_layout_name: String, | |
| saved_layouts: HashMap<String, SavedLayout>, | |
| // Not serialized since it doesn't need to be saved to disk on program exit | |
| #[serde(skip)] | |
| session: SessionState, | |
| // In a real program, there would be a lot more content in this struct. | |
| // ... | |
| } | |
| impl Default for MyAppState { | |
| fn default() -> Self { | |
| Self { | |
| current_layout_name: "Default".to_string(), | |
| saved_layouts: HashMap::new(), | |
| session: Default::default(), | |
| } | |
| } | |
| } | |
| impl MyAppState { | |
| fn get_open_tabs(&self, dock_state: &DockState<TabName>) -> Vec<TabName> { | |
| dock_state | |
| .iter_all_tabs() | |
| .map(|(_, tab_name)| tab_name.clone()) | |
| .collect() | |
| } | |
| fn get_closed_tabs(&self, dock_state: &DockState<TabName>) -> Vec<TabName> { | |
| let open_tabs: std::collections::HashSet<_> = | |
| self.get_open_tabs(dock_state).into_iter().collect(); | |
| TabName::iter() | |
| .filter(|tab| !open_tabs.contains(tab)) | |
| .collect() | |
| } | |
| fn generate_random_name(&self) -> String { | |
| let colors = [ | |
| "Red", "Blue", "Green", "Purple", "Orange", "Yellow", "Pink", "Cyan", "Magenta", | |
| "Lime", "Teal", "Navy", "Maroon", "Olive", "Silver", "Gold", "Coral", "Indigo", | |
| "Violet", "Crimson", | |
| ]; | |
| let animals = [ | |
| "Tiger", "Eagle", "Dolphin", "Wolf", "Fox", "Bear", "Hawk", "Lion", "Shark", "Falcon", | |
| "Panther", "Raven", "Lynx", "Cobra", "Moose", "Otter", "Jaguar", "Whale", "Bison", | |
| "Falcon", | |
| ]; | |
| let adjectives = [ | |
| "Swift", "Bold", "Calm", "Bright", "Sharp", "Strong", "Quick", "Wise", "Fierce", | |
| "Gentle", "Noble", "Silent", "Rapid", "Steady", "Clever", "Brave", "Agile", "Mighty", | |
| "Alert", "Graceful", | |
| ]; | |
| use std::collections::hash_map::DefaultHasher; | |
| use std::hash::{Hash, Hasher}; | |
| let mut hasher = DefaultHasher::new(); | |
| std::time::SystemTime::now().hash(&mut hasher); | |
| self.saved_layouts.len().hash(&mut hasher); | |
| let seed = hasher.finish(); | |
| let color = colors[(seed % colors.len() as u64) as usize]; | |
| let animal = animals[((seed >> 8) % animals.len() as u64) as usize]; | |
| let adjective = adjectives[((seed >> 16) % adjectives.len() as u64) as usize]; | |
| format!("{} {} {}", adjective, color, animal) | |
| } | |
| fn update_message_timers(&mut self) { | |
| if let Some(timer) = self.session.export_success_timer { | |
| if timer.elapsed() > std::time::Duration::from_secs(3) { | |
| self.session.export_success_message = None; | |
| self.session.export_success_timer = None; | |
| } | |
| } | |
| if let Some(timer) = self.session.import_error_timer { | |
| if timer.elapsed() > std::time::Duration::from_secs(5) { | |
| self.session.import_error_message = None; | |
| self.session.import_error_timer = None; | |
| } | |
| } | |
| if let Some(timer) = self.session.save_error_timer { | |
| if timer.elapsed() > std::time::Duration::from_secs(5) { | |
| self.session.save_error_message = None; | |
| self.session.save_error_timer = None; | |
| } | |
| } | |
| } | |
| fn show_menu(&mut self, ctx: &egui::Context) -> Vec<UiCommand> { | |
| let mut commands = Vec::new(); | |
| TopBottomPanel::top("menu_bar").show(ctx, |ui| { | |
| egui::MenuBar::new() | |
| .config(MenuConfig::new().close_behavior(PopupCloseBehavior::CloseOnClickOutside)) | |
| .ui(ui, |ui| { | |
| ui.menu_button("Settings", |ui| { | |
| ui.menu_button("Layouts", |ui| { | |
| // Use this label constructor rather than `ui.Label()` to ensure | |
| // the UI doesn't get into a bad state based on bad user input. | |
| // For example, pasting the exported layout into the name field | |
| // and "accidentally" saving it would make the UI so tall that | |
| // it wouldn't be possible to delete the offending layout! | |
| // | |
| // This doesn't sanitize the underlying name, so if the user | |
| // wants to store a massive unreadable string in this field, it is left | |
| // up to their disgression. | |
| egui::widgets::Label::new(format!( | |
| "Current Layout: {}", | |
| &self.current_layout_name | |
| )) | |
| .truncate() | |
| .ui(ui); | |
| ui.separator(); | |
| if ui.button("Save Current Layout").clicked() { | |
| commands.push(UiCommand::SaveLayout { | |
| name: self.current_layout_name.clone(), | |
| }); | |
| } | |
| ui.separator(); | |
| ui.horizontal(|ui| { | |
| if ui.button("Save Current Layout as:").clicked() { | |
| let name = if self.session.layout_name_input.trim().is_empty() { | |
| // TODO: Ensure that this doesn't overwrite any existing layouts. It's unlikely, but not impossible for it to happen by chance. | |
| self.generate_random_name() | |
| } else { | |
| self.session.layout_name_input.trim().to_string() | |
| }; | |
| commands.push(UiCommand::SaveLayout { name }); | |
| } | |
| ui.add( | |
| egui::TextEdit::singleline(&mut self.session.layout_name_input) | |
| .hint_text("Layout name, or empty for generated"), | |
| ) | |
| }); | |
| ui.separator(); | |
| if ui.button("Reset to Default").clicked() { | |
| commands.push(UiCommand::LoadLayout { name: None }); | |
| ui.close(); | |
| } | |
| ui.separator(); | |
| ui.horizontal(|ui| { | |
| if ui.button("Import Layout data:").clicked() { | |
| commands.push(UiCommand::ImportLayout { | |
| data: self.session.import_layout_buffer.clone(), | |
| }); | |
| } | |
| ui.add( | |
| egui::TextEdit::singleline( | |
| &mut self.session.import_layout_buffer, | |
| ) | |
| .hint_text("Paste layout data here"), | |
| ) | |
| }); | |
| ui.separator(); | |
| if !self.saved_layouts.is_empty() { | |
| ui.label("Saved Layouts:"); | |
| // Clone to avoid borrow checker issues | |
| let mut layouts: Vec<(String, SavedLayout)> = self | |
| .saved_layouts | |
| .iter() | |
| .map(|(k, v)| (k.clone(), v.clone())) | |
| .collect(); | |
| // Sort by the layout name. This is known to be unique since it just came from a HashMap. | |
| layouts.sort_by(|a, b| a.0.cmp(&b.0)); | |
| for (name, saved_layout) in layouts { | |
| ui.horizontal(|ui| { | |
| ui.set_min_width(200.0); | |
| if ui.button("Load").clicked() { | |
| commands.push(UiCommand::LoadLayout { | |
| name: Some(name.clone()), | |
| }); | |
| // I explicitly don't close the UI here because I don't | |
| // like that interaction. | |
| // ui.close(); | |
| } | |
| egui::widgets::Label::new(name.clone()).truncate().ui(ui); | |
| ui.with_layout( | |
| egui::Layout::right_to_left(egui::Align::Center), | |
| |ui| { | |
| let delete_enabled = !saved_layout.locked; | |
| if ui | |
| .add_enabled( | |
| delete_enabled, | |
| egui::Button::new("Delete"), | |
| ) | |
| .on_disabled_hover_text("Unprotect to delete") | |
| .clicked() | |
| { | |
| commands.push(UiCommand::DeleteLayout { | |
| name: name.clone(), | |
| }); | |
| } | |
| let mut locked = saved_layout.locked; | |
| if ui | |
| .checkbox(&mut locked, "Protect") | |
| .on_hover_text("Protect from deletion") | |
| .changed() | |
| { | |
| commands.push(UiCommand::SetLayoutLock { | |
| name: name.clone(), | |
| locked, | |
| }); | |
| } | |
| if ui | |
| .button("Export") | |
| .on_hover_text("Copy to clipboard") | |
| .clicked() | |
| { | |
| commands.push(UiCommand::ExportLayout { | |
| name: name.clone(), | |
| }); | |
| } | |
| }, | |
| ); | |
| }); | |
| } | |
| } else { | |
| ui.label("No saved layouts"); | |
| } | |
| if let Some(error_msg) = &self.session.import_error_message { | |
| ui.separator(); | |
| ui.colored_label(egui::Color32::RED, error_msg); | |
| } | |
| if let Some(error_msg) = &self.session.save_error_message { | |
| ui.separator(); | |
| ui.colored_label(egui::Color32::RED, error_msg); | |
| } | |
| if let Some(success_msg) = &self.session.export_success_message { | |
| ui.separator(); | |
| ui.colored_label(egui::Color32::GREEN, success_msg); | |
| } | |
| }); | |
| ui.menu_button("Theme", |ui| { | |
| egui::widgets::global_theme_preference_buttons(ui); | |
| }); | |
| }); | |
| }); | |
| }); | |
| commands | |
| } | |
| } | |
| #[derive(Serialize, Deserialize)] | |
| struct MyApp { | |
| state: MyAppState, | |
| dock: DockState<TabName>, | |
| } | |
| impl Default for MyApp { | |
| fn default() -> Self { | |
| // Create a 2x2 grid layout with all 4 tabs | |
| let mut dock_state = DockState::new(vec![TabName::Tab1]); | |
| // Split horizontally to create left and right sides | |
| let [left_side, right_side] = | |
| dock_state | |
| .main_surface_mut() | |
| .split_right(NodeIndex::root(), 0.5, vec![TabName::Tab2]); | |
| // Split left side vertically | |
| let [_top_left, _bottom_left] = | |
| dock_state | |
| .main_surface_mut() | |
| .split_below(left_side, 0.5, vec![TabName::Tab3]); | |
| // Split right side vertically | |
| let [_top_right, _bottom_right] = | |
| dock_state | |
| .main_surface_mut() | |
| .split_below(right_side, 0.5, vec![TabName::Tab4]); | |
| Self { | |
| state: MyAppState::default(), | |
| dock: dock_state, | |
| } | |
| } | |
| } | |
| #[derive(Debug, Clone, Serialize, Deserialize)] | |
| struct ExportedLayout { | |
| name: String, | |
| dock_state: DockState<TabName>, | |
| locked: bool, | |
| } | |
| impl MyApp { | |
| fn create_default_dock_state() -> DockState<TabName> { | |
| // Create a 2x2 grid layout with all 4 tabs | |
| let mut dock_state = DockState::new(vec![TabName::Tab1]); | |
| // Split horizontally to create left and right sides | |
| let [left_side, right_side] = | |
| dock_state | |
| .main_surface_mut() | |
| .split_right(NodeIndex::root(), 0.5, vec![TabName::Tab2]); | |
| // Split left side vertically | |
| let [_top_left, _bottom_left] = | |
| dock_state | |
| .main_surface_mut() | |
| .split_below(left_side, 0.5, vec![TabName::Tab3]); | |
| // Split right side vertically | |
| let [_top_right, _bottom_right] = | |
| dock_state | |
| .main_surface_mut() | |
| .split_below(right_side, 0.5, vec![TabName::Tab4]); | |
| dock_state | |
| } | |
| fn process_commands(&mut self, commands: Vec<UiCommand>, ctx: &egui::Context) { | |
| // TODO: Add a vscroll around the saved layout listing. Until then, limit the number of | |
| // layouts for the sake of a usable UI. | |
| const MAX_LAYOUTS: usize = 16; | |
| for command in commands { | |
| match command { | |
| UiCommand::SaveLayout { name } => { | |
| if name == "Default" { | |
| self.state.session.save_error_message = | |
| Some("Cannot modify the Default layout".to_string()); | |
| self.state.session.save_error_timer = Some(std::time::Instant::now()); | |
| continue; | |
| } | |
| if !self.state.saved_layouts.contains_key(&name) | |
| && self.state.saved_layouts.len() >= MAX_LAYOUTS | |
| { | |
| self.state.session.save_error_message = Some(format!( | |
| "No room for additional layouts. {} is the maximum.", | |
| MAX_LAYOUTS | |
| )); | |
| self.state.session.save_error_timer = Some(std::time::Instant::now()); | |
| continue; | |
| } | |
| let saved_layout = SavedLayout { | |
| dock_state: self.dock.clone(), | |
| locked: false, | |
| }; | |
| self.state.saved_layouts.insert(name.clone(), saved_layout); | |
| self.state.current_layout_name = name; | |
| self.state.session.layout_name_input.clear(); | |
| self.state.session.save_error_message = None; | |
| self.state.session.save_error_timer = None; | |
| } | |
| UiCommand::LoadLayout { name } => { | |
| if let Some(name) = name { | |
| if let Some(saved_layout) = self.state.saved_layouts.get(&name) { | |
| self.dock = saved_layout.dock_state.clone(); | |
| self.state.current_layout_name = name; | |
| } else { | |
| self.dock = Self::create_default_dock_state(); | |
| self.state.current_layout_name = "Default".to_string(); | |
| } | |
| } else { | |
| self.dock = Self::create_default_dock_state(); | |
| self.state.current_layout_name = "Default".to_string(); | |
| } | |
| } | |
| UiCommand::DeleteLayout { name } => { | |
| if let Some(saved_layout) = self.state.saved_layouts.get(&name) { | |
| if !saved_layout.locked { | |
| self.state.saved_layouts.remove(&name); | |
| } | |
| } | |
| } | |
| UiCommand::ExportLayout { name } => { | |
| if let Some(saved_layout) = self.state.saved_layouts.get(&name) { | |
| let exported = ExportedLayout { | |
| name: name.clone(), | |
| dock_state: saved_layout.dock_state.clone(), | |
| locked: saved_layout.locked, | |
| }; | |
| match ron::ser::to_string_pretty( | |
| &exported, | |
| ron::ser::PrettyConfig::default(), | |
| ) { | |
| Ok(ron_data) => { | |
| ctx.copy_text(ron_data); | |
| self.state.session.import_error_message = None; | |
| self.state.session.import_error_timer = None; | |
| self.state.session.export_success_message = | |
| Some("Layout copied to clipboard!".to_string()); | |
| self.state.session.export_success_timer = | |
| Some(std::time::Instant::now()); | |
| } | |
| Err(e) => { | |
| self.state.session.import_error_message = | |
| Some(format!("Failed to export layout: {}", e)); | |
| self.state.session.import_error_timer = | |
| Some(std::time::Instant::now()); | |
| self.state.session.export_success_message = None; | |
| self.state.session.export_success_timer = None; | |
| } | |
| } | |
| } | |
| } | |
| UiCommand::ImportLayout { data } => { | |
| if data.trim().is_empty() { | |
| self.state.session.import_error_message = | |
| Some("No layout data provided".to_string()); | |
| self.state.session.import_error_timer = Some(std::time::Instant::now()); | |
| continue; | |
| } | |
| if self.state.saved_layouts.len() >= MAX_LAYOUTS { | |
| self.state.session.save_error_message = Some(format!( | |
| "No room for additional layouts. {} is the maximum.", | |
| MAX_LAYOUTS | |
| )); | |
| continue; | |
| } | |
| match ron::from_str::<ExportedLayout>(&data) { | |
| Ok(imported) => { | |
| let original_name = imported.name.clone(); | |
| let mut final_name = imported.name; | |
| if self.state.saved_layouts.contains_key(&final_name) { | |
| final_name = format!("{} (imported)", original_name); | |
| let mut counter = 2; | |
| while self.state.saved_layouts.contains_key(&final_name) { | |
| final_name = | |
| format!("{} (imported {})", original_name, counter); | |
| counter += 1; | |
| } | |
| } | |
| let saved_layout = SavedLayout { | |
| dock_state: imported.dock_state.clone(), | |
| locked: imported.locked, | |
| }; | |
| self.state | |
| .saved_layouts | |
| .insert(final_name.clone(), saved_layout); | |
| self.dock = imported.dock_state; | |
| self.state.current_layout_name = final_name; | |
| self.state.session.import_error_message = None; | |
| self.state.session.import_error_timer = None; | |
| self.state.session.export_success_message = None; | |
| self.state.session.export_success_timer = None; | |
| self.state.session.import_layout_buffer.clear(); | |
| } | |
| Err(e) => { | |
| self.state.session.import_error_message = | |
| Some(format!("Failed to parse layout: {}", e)); | |
| self.state.session.import_error_timer = Some(std::time::Instant::now()); | |
| self.state.session.export_success_message = None; | |
| self.state.session.export_success_timer = None; | |
| } | |
| } | |
| } | |
| UiCommand::AddTab { tab, surface, node } => { | |
| self.dock[surface].set_focused_node(node); | |
| self.dock[surface].push_to_focused_leaf(tab); | |
| } | |
| UiCommand::AddTabOnParent { | |
| new_tab, | |
| parent_tab, | |
| } => { | |
| if let Some((surface, node, _)) = self.dock.find_tab(&parent_tab) { | |
| self.dock[surface].set_focused_node(node); | |
| self.dock[surface].push_to_focused_leaf(new_tab); | |
| } else { | |
| eprintln!( | |
| "Attempted to add tab {:?} to parent {:?}, but parent tab wasn't found", | |
| new_tab, parent_tab | |
| ) | |
| } | |
| } | |
| UiCommand::SetLayoutLock { name, locked } => { | |
| if let Some(layout) = self.state.saved_layouts.get_mut(&name) { | |
| layout.locked = locked; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| // A custom TabViewer that has access to all of the app state, as well as | |
| // access to pre-computed information about DockState (the `available_tabs`). | |
| // Since this struct cannot have a reference to DockState (it would cause borrow issues), | |
| // it populates `commands`. | |
| // Commands then need to be handled by the caller after `DockArea::new(dock_state)::show(tab_viewer);` | |
| struct MyAppTabViewer<'a> { | |
| state: &'a mut MyAppState, | |
| available_tabs: Vec<TabName>, | |
| commands: &'a mut Vec<UiCommand>, | |
| } | |
| impl<'a> TabViewer for MyAppTabViewer<'a> { | |
| type Tab = TabName; | |
| fn title(&mut self, tab: &mut Self::Tab) -> WidgetText { | |
| tab.display_name().into() | |
| } | |
| fn ui(&mut self, ui: &mut Ui, tab: &mut Self::Tab) { | |
| match tab { | |
| TabName::Tab1 => { | |
| ui.vertical_centered(|ui| { | |
| ui.heading("Tab 1"); | |
| ui.label("First demo tab"); | |
| ui.label(format!( | |
| "There are {} saved layouts", | |
| self.state.saved_layouts.len() | |
| )); | |
| }); | |
| } | |
| TabName::Tab2 => { | |
| ui.vertical_centered(|ui| { | |
| ui.heading("Tab 2"); | |
| ui.label("Second demo tab"); | |
| ui.label(format!( | |
| "The current layout is named: {}", | |
| self.state.current_layout_name | |
| )); | |
| }); | |
| } | |
| TabName::Tab3 => { | |
| ui.vertical_centered(|ui| { | |
| ui.heading("Tab 3"); | |
| ui.label("Third demo tab"); | |
| if !self.state.session.import_layout_buffer.is_empty() { | |
| ui.label("There is text in the import_layout_buffer"); | |
| } | |
| }); | |
| } | |
| TabName::Tab4 => { | |
| ui.vertical_centered(|ui| { | |
| ui.heading("Tab 4"); | |
| ui.label("Fourth demo tab"); | |
| for openable_tab in self.available_tabs.iter() { | |
| if ui | |
| .button(format!("Open tab {}", openable_tab.display_name())) | |
| .clicked() | |
| { | |
| self.commands.push(UiCommand::AddTabOnParent { | |
| new_tab: openable_tab.clone(), | |
| parent_tab: tab.clone(), | |
| }) | |
| } | |
| } | |
| }); | |
| } | |
| } | |
| } | |
| fn on_close(&mut self, _tab: &mut Self::Tab) -> OnCloseResponse { | |
| OnCloseResponse::Close | |
| } | |
| fn on_add(&mut self, _surface: SurfaceIndex, _node: NodeIndex) { | |
| // This method is called when the add button is pressed. If you want that to immediately | |
| // open a tab, then push a command here. | |
| // For this program I want the add button to open the popup (the dropdown menu) | |
| // to select which tab to open, so nothing needs to be done here. | |
| } | |
| fn add_popup(&mut self, ui: &mut Ui, surface: SurfaceIndex, node: NodeIndex) { | |
| if self.available_tabs.is_empty() { | |
| ui.label("No available tabs to open!"); | |
| return; | |
| } | |
| for tab in &self.available_tabs { | |
| if ui.button(tab.display_name()).clicked() { | |
| self.commands.push(UiCommand::AddTab { | |
| tab: tab.clone(), | |
| surface, | |
| node, | |
| }); | |
| ui.close(); | |
| } | |
| } | |
| } | |
| } | |
| impl eframe::App for MyApp { | |
| fn save(&mut self, storage: &mut dyn eframe::Storage) { | |
| eframe::set_value(storage, eframe::APP_KEY, self); | |
| } | |
| fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { | |
| self.state.update_message_timers(); | |
| let mut commands = self.state.show_menu(ctx); | |
| CentralPanel::default().show(ctx, |ui| { | |
| // Pre-compute available tabs before creating the TabViewer. | |
| // Because everything is within MyApp, we cannot provide `self.dock` into DockArea::new | |
| // and also provide it into tab_viewer without being blocked by Rust's borrow checker. | |
| // Making a copy of relevant information within DockState is necessary. | |
| let available_tabs = self.state.get_closed_tabs(&self.dock); | |
| // This option isn't required, but I like removing the add tab interactions | |
| // when there aren't actually any tabs available. This assumes that all of your tabs | |
| // are unique: there should never be multiple of the same tab open. This may not | |
| // be the case for your program! | |
| let show_add_buttons = !available_tabs.is_empty(); | |
| let mut tab_viewer_commands = Vec::new(); | |
| let mut tab_viewer = MyAppTabViewer { | |
| state: &mut self.state, | |
| available_tabs, | |
| commands: &mut tab_viewer_commands, | |
| }; | |
| DockArea::new(&mut self.dock) | |
| .show_add_buttons(show_add_buttons) | |
| .show_add_popup(show_add_buttons) | |
| .show_close_buttons(true) | |
| .show_inside(ui, &mut tab_viewer); | |
| commands.extend(tab_viewer_commands) | |
| }); | |
| self.process_commands(commands, ctx); | |
| } | |
| } | |
| fn main() -> eframe::Result<()> { | |
| let options = NativeOptions { | |
| viewport: ViewportBuilder::default() | |
| .with_inner_size(vec2(1200.0, 800.0)) | |
| .with_title("Dock Layout Demo"), | |
| ..Default::default() | |
| }; | |
| eframe::run_native( | |
| "Dock Layout Demo", | |
| options, | |
| Box::new(|cc| { | |
| if let Some(storage) = cc.storage { | |
| if let Some(app) = eframe::get_value::<MyApp>(storage, eframe::APP_KEY) { | |
| return Ok(Box::new(app)); | |
| } | |
| } | |
| Ok(Box::new(MyApp::default())) | |
| }), | |
| ) | |
| } |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
egui_dock_snapshot_demo2.noaudio.webm