Skip to main content

grimdock/
tab.rs

1use crate::{
2    style::TabStyleOverride,
3    tree::{PaneId, PaneRole},
4};
5
6/// Per-tab drop constraints evaluated against the destination pane.
7#[derive(Clone, Debug, PartialEq, Eq)]
8#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
9pub struct TabDropPolicy {
10    /// If set, this tab may only be dropped into the given pane.
11    pub locked_to_pane: Option<PaneId>,
12    /// If set, this tab may only be dropped into panes with the given role.
13    pub locked_to_role: Option<PaneRole>,
14    /// If set, this tab may only be dropped into panes in this allow-list.
15    pub allowed_panes: Option<Vec<PaneId>>,
16    /// If set, this tab may only be dropped into panes with roles in this allow-list.
17    pub allowed_roles: Option<Vec<PaneRole>>,
18    /// Panes in this block-list reject this tab even if otherwise allowed.
19    pub blocked_panes: Vec<PaneId>,
20    /// Roles in this block-list reject this tab even if otherwise allowed.
21    pub blocked_roles: Vec<PaneRole>,
22}
23
24impl TabDropPolicy {
25    /// Return whether this tab may be dropped into the given pane target.
26    ///
27    /// Pane identity and semantic role are evaluated together. This lets
28    /// applications express either "only this pane instance" or "any pane with
29    /// this role" style rules.
30    pub fn allows_target(&self, pane_id: PaneId, role: Option<PaneRole>) -> bool {
31        if let Some(locked_to_pane) = self.locked_to_pane {
32            return locked_to_pane == pane_id;
33        }
34
35        if let Some(locked_to_role) = self.locked_to_role {
36            return role == Some(locked_to_role);
37        }
38
39        if let Some(allowed_panes) = &self.allowed_panes {
40            if !allowed_panes.contains(&pane_id) {
41                return false;
42            }
43        }
44
45        if let Some(allowed_roles) = &self.allowed_roles {
46            if !role.is_some_and(|role| allowed_roles.contains(&role)) {
47                return false;
48            }
49        }
50
51        if self.blocked_panes.contains(&pane_id) {
52            return false;
53        }
54
55        !role.is_some_and(|role| self.blocked_roles.contains(&role))
56    }
57}
58
59impl Default for TabDropPolicy {
60    fn default() -> Self {
61        Self {
62            locked_to_pane: None,
63            locked_to_role: None,
64            allowed_panes: None,
65            allowed_roles: None,
66            blocked_panes: Vec::new(),
67            blocked_roles: Vec::new(),
68        }
69    }
70}
71
72/// Leading tab icon content rendered before the tab title.
73#[derive(Clone, Debug, PartialEq)]
74#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
75pub enum TabIcon {
76    /// Short text marker such as ASCII tokens or compact glyphs.
77    Text(String),
78    /// Texture-backed image icon with an explicit display size.
79    Texture {
80        texture_id: egui::TextureId,
81        size: egui::Vec2,
82    },
83}
84
85/// A single tab that lives inside a pane.
86///
87/// `T` is the caller-supplied identifier — must be `Clone + 'static`.
88#[derive(Clone, Debug, PartialEq)]
89#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
90pub struct Tab<T: Clone + 'static> {
91    pub title: String,
92    pub id: T,
93    /// Optional leading icon rendered before the title in tab headers.
94    pub icon: Option<TabIcon>,
95    /// Whether the tab may be dragged to another pane.
96    pub draggable: bool,
97    /// Per-tab destination constraints evaluated during drag-and-drop.
98    pub drop_policy: TabDropPolicy,
99    /// Whether the tab may be closed from the header UI.
100    pub closable: bool,
101    /// Optional visual overrides for this tab.
102    pub style_override: Option<TabStyleOverride>,
103}
104
105impl<T: Clone + 'static> Tab<T> {
106    /// Create a new tab with a title and caller-owned identifier.
107    pub fn new(title: impl Into<String>, id: T) -> Self {
108        Self {
109            title: title.into(),
110            id,
111            icon: None,
112            draggable: true,
113            drop_policy: TabDropPolicy::default(),
114            closable: false,
115            style_override: None,
116        }
117    }
118
119    /// Set a short text marker as the leading tab visual.
120    pub fn with_leading_visual(mut self, leading_visual: impl Into<String>) -> Self {
121        self.icon = Some(TabIcon::Text(leading_visual.into()));
122        self
123    }
124
125    /// Set the leading icon payload directly.
126    pub fn with_icon(mut self, icon: TabIcon) -> Self {
127        self.icon = Some(icon);
128        self
129    }
130
131    /// Set a texture-backed leading icon.
132    pub fn with_icon_texture(mut self, texture_id: egui::TextureId, size: egui::Vec2) -> Self {
133        self.icon = Some(TabIcon::Texture { texture_id, size });
134        self
135    }
136
137    /// Control whether this tab may be dragged at all.
138    pub fn with_draggable(mut self, draggable: bool) -> Self {
139        self.draggable = draggable;
140        self
141    }
142
143    /// Replace the full tab drop-policy payload.
144    pub fn with_drop_policy(mut self, drop_policy: TabDropPolicy) -> Self {
145        self.drop_policy = drop_policy;
146        self
147    }
148
149    /// Lock the tab to a specific pane instance.
150    pub fn with_locked_pane(mut self, pane_id: PaneId) -> Self {
151        self.drop_policy.locked_to_pane = Some(pane_id);
152        self
153    }
154
155    /// Lock the tab to panes with a specific semantic role.
156    pub fn with_locked_role(mut self, role: PaneRole) -> Self {
157        self.drop_policy.locked_to_role = Some(role);
158        self
159    }
160
161    /// Restrict the tab to a set of pane identifiers.
162    pub fn with_allowed_drop_panes(
163        mut self,
164        allowed_panes: impl IntoIterator<Item = PaneId>,
165    ) -> Self {
166        self.drop_policy.allowed_panes = Some(allowed_panes.into_iter().collect());
167        self
168    }
169
170    /// Reject drops into a set of pane identifiers.
171    pub fn with_blocked_drop_panes(
172        mut self,
173        blocked_panes: impl IntoIterator<Item = PaneId>,
174    ) -> Self {
175        self.drop_policy.blocked_panes = blocked_panes.into_iter().collect();
176        self
177    }
178
179    /// Restrict the tab to a set of pane roles.
180    pub fn with_allowed_drop_roles(
181        mut self,
182        allowed_roles: impl IntoIterator<Item = PaneRole>,
183    ) -> Self {
184        self.drop_policy.allowed_roles = Some(allowed_roles.into_iter().collect());
185        self
186    }
187
188    /// Reject drops into panes with a set of semantic roles.
189    pub fn with_blocked_drop_roles(
190        mut self,
191        blocked_roles: impl IntoIterator<Item = PaneRole>,
192    ) -> Self {
193        self.drop_policy.blocked_roles = blocked_roles.into_iter().collect();
194        self
195    }
196
197    /// Control whether the tab may be closed from the built-in header UI.
198    pub fn with_closable(mut self, closable: bool) -> Self {
199        self.closable = closable;
200        self
201    }
202
203    /// Apply a per-tab style override used by the built-in header renderer.
204    pub fn with_style_override(mut self, style_override: TabStyleOverride) -> Self {
205        self.style_override = Some(style_override);
206        self
207    }
208}