//! Policy controls example: //! `cargo run --example 6_policy_controls` use eframe::egui; use grimdock::{ DropPolicy, PaneAnchor, PaneBuilder, PaneId, PaneMenuAction, PaneOptions, PaneRole, PanelContext, PanelStyle, PanelTree, Tab, TabDropPolicy, }; fn main() -> eframe::Result<()> { let options = eframe::NativeOptions { viewport: egui::ViewportBuilder::default() .with_title("grimdock 6_policy_controls") .with_inner_size([1180.0, 720.0]), ..Default::default() }; eframe::run_native( "grimdock 6_policy_controls", options, Box::new(|_cc| Ok(Box::new(App::new()))), ) } #[derive(Clone, PartialEq, Eq, Debug)] enum TabId { Files, Editor, Search, Terminal, } struct App { tree: PanelTree, style: PanelStyle, files_home_pane: PaneId, editor_home_pane: PaneId, terminal_pane: PaneId, persist_files_pane: bool, lock_files_layout: bool, disable_files_drops: bool, disable_terminal_resize: bool, disable_editor_reorder: bool, disable_editor_drag_out: bool, freeze_search_tab: bool, lock_search_to_files_pane: bool, allow_search_only_files_and_editor: bool, block_search_from_terminal: bool, lock_search_to_sidebar_role: bool, allow_search_only_sidebar_and_editor_roles: bool, block_search_from_terminal_role: bool, pane_action_log: Vec, } impl App { fn new() -> Self { let (tree, files_home_pane, editor_home_pane, terminal_pane) = make_tree(); Self { tree, style: PanelStyle::default(), files_home_pane, editor_home_pane, terminal_pane, persist_files_pane: true, lock_files_layout: false, disable_files_drops: false, disable_terminal_resize: false, disable_editor_reorder: false, disable_editor_drag_out: false, freeze_search_tab: false, lock_search_to_files_pane: false, allow_search_only_files_and_editor: false, block_search_from_terminal: false, lock_search_to_sidebar_role: false, allow_search_only_sidebar_and_editor_roles: false, block_search_from_terminal_role: false, pane_action_log: Vec::new(), } } fn reset_layout(&mut self) { let (tree, files_home_pane, editor_home_pane, terminal_pane) = make_tree(); self.tree = tree; self.files_home_pane = files_home_pane; self.editor_home_pane = editor_home_pane; self.terminal_pane = terminal_pane; } fn update_pane_options( &mut self, pane_id: PaneId, update: impl FnOnce(&mut PaneOptions), ) { if let Some(mut pane) = self.tree.pane_mut(pane_id) { let mut options = pane.options(); update(&mut options); pane.set_options(options); } } fn apply_policy_toggles(&mut self) { let persist_files_pane = self.persist_files_pane; let lock_files_layout = self.lock_files_layout; let disable_files_drops = self.disable_files_drops; let disable_terminal_resize = self.disable_terminal_resize; let disable_editor_reorder = self.disable_editor_reorder; let disable_editor_drag_out = self.disable_editor_drag_out; let freeze_search_tab = self.freeze_search_tab; let lock_search_to_files_pane = self.lock_search_to_files_pane; let allow_search_only_files_and_editor = self.allow_search_only_files_and_editor; let block_search_from_terminal = self.block_search_from_terminal; let lock_search_to_sidebar_role = self.lock_search_to_sidebar_role; let allow_search_only_sidebar_and_editor_roles = self.allow_search_only_sidebar_and_editor_roles; let block_search_from_terminal_role = self.block_search_from_terminal_role; let files_home_pane = self.files_home_pane; let editor_home_pane = self.editor_home_pane; let terminal_pane = self.terminal_pane; self.update_pane_options(files_home_pane, |options| { options.persist_when_empty = persist_files_pane; options.lock_layout = lock_files_layout; options.drop_policy = if disable_files_drops { DropPolicy::none() } else { DropPolicy::all() }; }); self.update_pane_options(editor_home_pane, |options| { options.allow_tab_reorder = !disable_editor_reorder; options.allow_tab_drag_out = !disable_editor_drag_out; }); self.update_pane_options(terminal_pane, |options| { options.allow_resize = !disable_terminal_resize; }); if let Some(search_pane_id) = self.tree.find_pane_containing(&TabId::Search) { if let Some(mut pane) = self.tree.pane_mut(search_pane_id) { if let Some(tab) = pane.tabs_mut().iter_mut().find(|tab| tab.id == TabId::Search) { tab.draggable = !freeze_search_tab; let mut drop_policy = TabDropPolicy::default(); if lock_search_to_files_pane { drop_policy.locked_to_pane = Some(files_home_pane); } else if lock_search_to_sidebar_role { drop_policy.locked_to_role = Some(PaneRole::Sidebar); } else if allow_search_only_files_and_editor { drop_policy.allowed_panes = Some(vec![files_home_pane, editor_home_pane]); } else if allow_search_only_sidebar_and_editor_roles { drop_policy.allowed_roles = Some(vec![PaneRole::Sidebar, PaneRole::Editor]); } if block_search_from_terminal { drop_policy.blocked_panes.push(terminal_pane); } if block_search_from_terminal_role { drop_policy.blocked_roles.push(PaneRole::Terminal); } tab.drop_policy = drop_policy; } } } } fn pane_present(&self, pane_id: PaneId) -> bool { self.tree.pane(pane_id).is_some() } } impl eframe::App for App { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { self.apply_policy_toggles(); egui::Panel::left("policy_controls") .resizable(false) .default_width(320.0) .show(ctx, |ui| { ui.heading("Policy controls"); ui.label("This example separates pane policy from tab policy."); ui.separator(); ui.label("Pane policy"); ui.checkbox(&mut self.persist_files_pane, "Persist Files pane when empty"); ui.checkbox(&mut self.lock_files_layout, "Lock Files pane layout"); ui.checkbox(&mut self.disable_files_drops, "Reject drops into Files pane"); ui.checkbox(&mut self.disable_terminal_resize, "Disable Terminal resize"); ui.checkbox(&mut self.disable_editor_reorder, "Disable Editor pane reorder"); ui.checkbox(&mut self.disable_editor_drag_out, "Disable Editor pane drag-out"); ui.separator(); ui.label("Tab policy"); ui.checkbox(&mut self.freeze_search_tab, "Make Search tab immovable"); ui.checkbox(&mut self.lock_search_to_files_pane, "Lock Search tab to Files pane ID"); ui.checkbox(&mut self.lock_search_to_sidebar_role, "Lock Search tab to Sidebar role"); ui.checkbox( &mut self.allow_search_only_files_and_editor, "Allow Search only in Files and Editor pane IDs", ); ui.checkbox( &mut self.allow_search_only_sidebar_and_editor_roles, "Allow Search only in Sidebar and Editor roles", ); ui.checkbox(&mut self.block_search_from_terminal, "Block Search from Terminal pane ID"); ui.checkbox( &mut self.block_search_from_terminal_role, "Block Search from Terminal role", ); ui.separator(); if ui.button("Reset layout").clicked() { self.reset_layout(); } ui.separator(); ui.label(format!( "Files home pane present: {}", if self.pane_present(self.files_home_pane) { "yes" } else { "no" } )); ui.label(format!( "Files anchor owner: {:?}", self.tree.find_pane_with_anchor(PaneAnchor::Left) )); ui.label(format!( "Terminal anchor owner: {:?}", self.tree.find_pane_with_anchor(PaneAnchor::Bottom) )); ui.label(format!( "Sidebar role owner: {:?}", self.tree.find_pane_with_role(PaneRole::Sidebar) )); ui.label(format!( "Terminal role owner: {:?}", self.tree.find_pane_with_role(PaneRole::Terminal) )); ui.label(format!( "Search tab currently in pane: {:?}", self.tree.find_pane_containing(&TabId::Search) )); ui.separator(); ui.label("What to try:"); ui.label("1. Move Files out of its home pane. With persistence on, the anchored left pane stays as an empty drop target."); ui.label("2. Turn off Files persistence, empty that pane, and notice the pane can now disappear."); ui.label("3. Lock Search to the Files pane, then try dropping it into Editor or Terminal."); ui.label("4. Switch between pane-ID and pane-role targeting to compare semantic versus identity-based policy."); if let Some(last) = self.pane_action_log.last() { ui.separator(); ui.label(format!("Last custom pane action: {last}")); } }); egui::CentralPanel::default() .frame(egui::Frame::NONE) .show(ctx, |ui| { let output = PanelContext::new(ui, &mut self.tree, &self.style) .with_pane_menu_provider(&|pane_id, tree| { let mut actions = vec![PaneMenuAction::new("inspect", "Inspect pane")]; if tree.find_pane_with_anchor(PaneAnchor::Left) == Some(pane_id) { actions.push(PaneMenuAction::new("mark_left", "Mark left anchor")); } actions }) .show(|ui, tab_id| match tab_id { TabId::Files => { ui.heading("Files"); ui.label("This pane demonstrates pane persistence, pane locking, and pane drop policy."); } TabId::Editor => { ui.heading("Editor"); ui.label("This pane demonstrates pane-level reorder and drag-out controls."); } TabId::Search => { ui.heading("Search"); ui.label("This tab demonstrates draggable state plus pane lock / allow-list / block-list targeting."); } TabId::Terminal => { ui.heading("Terminal"); ui.label("This pane demonstrates resize locking and can also be blocked as a tab destination."); } }); for action in output.pane_actions { self.pane_action_log .push(format!("{} on pane {:?}", action.action_id, action.pane_id)); } }); } } fn make_tree() -> (PanelTree, PaneId, PaneId, PaneId) { let mut tree = PanelTree::new(vec![ Tab::new("editor", TabId::Editor).with_leading_visual(">"), Tab::new("search", TabId::Search).with_leading_visual("?"), ]); let files_home_pane = tree.ensure_pane_at_anchor( PaneAnchor::Left, PaneBuilder::new(Tab::new("files", TabId::Files).with_leading_visual("#")), ); let editor_home_pane = tree .find_pane_containing(&TabId::Editor) .expect("editor pane should exist after initial split"); let terminal_pane = tree.ensure_pane_at_anchor( PaneAnchor::Bottom, PaneBuilder::new(Tab::new("terminal", TabId::Terminal).with_leading_visual("$")), ); if let Some(mut pane) = tree.pane_mut(files_home_pane) { let mut options = pane.options(); options.persist_when_empty = true; options.role = Some(PaneRole::Sidebar); pane.set_options(options); } if let Some(mut pane) = tree.pane_mut(editor_home_pane) { let mut options = pane.options(); options.role = Some(PaneRole::Editor); pane.set_options(options); } if let Some(mut pane) = tree.pane_mut(terminal_pane) { let mut options = pane.options(); options.role = Some(PaneRole::Terminal); pane.set_options(options); } (tree, files_home_pane, editor_home_pane, terminal_pane) }