1use crate::clip::ClipRect;
8use crate::draw_list::{DrawCommand, QuadCommand, TextCommand};
9use crate::plugin::UiPlugin;
10use crate::plugin::registry::{
11 TraversalBehavior, WidgetOverflow, WidgetRenderContext, WidgetTypeDescriptor,
12 WidgetTypeRegistry,
13};
14use crate::style::Overflow;
15use crate::tree::{NodeId, UiTree};
16use crate::widgets::docking::tabs::{CHAR_WIDTH_FACTOR, CLOSE_BUTTON_MARGIN};
17use crate::widgets::docking::{
18 DEFAULT_CLOSE_BUTTON_SIZE, DEFAULT_TAB_PADDING, DockAnimationState, DockSplitter, DockTabs,
19 DockingContext, DragManager, DropZoneDetector,
20};
21use astrelis_core::math::Vec2;
22use astrelis_render::Color;
23use std::any::Any;
24
25pub struct DockingPlugin {
34 pub drag_manager: DragManager,
36 pub hovered_splitter: Option<NodeId>,
38 pub drop_zone_detector: DropZoneDetector,
40 pub cross_container_preview: Option<CrossContainerPreview>,
42 pub docking_context: DockingContext,
44 pub dock_animations: DockAnimationState,
46 pub scrollbar_drag_node: Option<NodeId>,
48}
49
50#[derive(Debug, Clone, Copy)]
52pub struct CrossContainerPreview {
53 pub target_node: NodeId,
55 pub target_layout: crate::tree::LayoutRect,
57 pub zone: crate::widgets::docking::DockZone,
59 pub preview_bounds: crate::tree::LayoutRect,
61 pub insert_index: Option<usize>,
63}
64
65impl DockingPlugin {
66 pub fn new() -> Self {
68 Self {
69 drag_manager: DragManager::new(),
70 hovered_splitter: None,
71 drop_zone_detector: DropZoneDetector::new(),
72 cross_container_preview: None,
73 docking_context: DockingContext::new(),
74 dock_animations: DockAnimationState::new(),
75 scrollbar_drag_node: None,
76 }
77 }
78
79 pub fn invalidate_cache(&mut self) {
81 self.docking_context.invalidate();
82 }
83
84 pub fn update_animations(&mut self, dt: f32) -> bool {
87 self.dock_animations.update(dt)
88 }
89
90 pub fn is_dragging(&self) -> bool {
92 self.drag_manager.is_dragging()
93 }
94
95 pub fn invalidate_removed_nodes(&mut self, tree: &UiTree) {
97 if let Some(id) = self.hovered_splitter
98 && !tree.node_exists(id)
99 {
100 self.hovered_splitter = None;
101 }
102 if let Some(ref p) = self.cross_container_preview
103 && !tree.node_exists(p.target_node)
104 {
105 self.cross_container_preview = None;
106 }
107 if let Some(id) = self.scrollbar_drag_node
108 && !tree.node_exists(id)
109 {
110 self.scrollbar_drag_node = None;
111 }
112 }
113}
114
115impl Default for DockingPlugin {
116 fn default() -> Self {
117 Self::new()
118 }
119}
120
121impl UiPlugin for DockingPlugin {
122 fn name(&self) -> &str {
123 "docking"
124 }
125
126 fn register_widgets(&self, registry: &mut WidgetTypeRegistry) {
127 registry.register::<DockSplitter>(
128 WidgetTypeDescriptor::new("DockSplitter").with_render(render_dock_splitter),
129 );
130 registry.register::<DockTabs>(
131 WidgetTypeDescriptor::new("DockTabs")
132 .with_render(render_dock_tabs)
133 .with_traversal(dock_tabs_traversal)
134 .with_overflow(dock_tabs_overflow),
135 );
136 }
137
138 fn post_layout(&mut self, _tree: &mut UiTree) {
139 }
143
144 fn update(&mut self, dt: f32, _tree: &mut UiTree) {
145 self.update_animations(dt);
146 }
147
148 fn as_any(&self) -> &dyn Any {
149 self
150 }
151
152 fn as_any_mut(&mut self) -> &mut dyn Any {
153 self
154 }
155}
156
157pub fn render_dock_splitter(
162 widget: &dyn Any,
163 ctx: &mut WidgetRenderContext<'_>,
164) -> Vec<DrawCommand> {
165 let splitter = widget.downcast_ref::<DockSplitter>().unwrap();
166 let mut commands = Vec::new();
167
168 let sep_bounds = splitter.separator_bounds(&crate::tree::LayoutRect {
169 x: ctx.abs_position.x,
170 y: ctx.abs_position.y,
171 width: ctx.layout_size.x,
172 height: ctx.layout_size.y,
173 });
174
175 let sep_color = splitter.current_separator_color();
176
177 commands.push(DrawCommand::Quad(
178 QuadCommand::filled(
179 Vec2::new(sep_bounds.x, sep_bounds.y),
180 Vec2::new(sep_bounds.width, sep_bounds.height),
181 sep_color,
182 ctx.parent_z_index,
183 )
184 .with_clip(ctx.clip_rect),
185 ));
186
187 commands
188}
189
190pub fn render_dock_tabs(widget: &dyn Any, ctx: &mut WidgetRenderContext<'_>) -> Vec<DrawCommand> {
195 let tabs = widget.downcast_ref::<DockTabs>().unwrap();
196 let mut commands = Vec::new();
197
198 let abs_layout = crate::tree::LayoutRect {
199 x: ctx.abs_position.x,
200 y: ctx.abs_position.y,
201 width: ctx.layout_size.x,
202 height: ctx.layout_size.y,
203 };
204
205 let bar_bounds = tabs.tab_bar_bounds(&abs_layout);
207 commands.push(DrawCommand::Quad(
208 QuadCommand::filled(
209 Vec2::new(bar_bounds.x, bar_bounds.y),
210 Vec2::new(bar_bounds.width, bar_bounds.height),
211 tabs.theme.tab_bar_color,
212 ctx.parent_z_index,
213 )
214 .with_clip(ctx.clip_rect),
215 ));
216
217 let tab_row = tabs.tab_row_bounds(&abs_layout);
219 let tab_row_clip = ClipRect::from_bounds(tab_row.x, tab_row.y, tab_row.width, tab_row.height);
220 let tab_clip = ctx.clip_rect.intersect(&tab_row_clip);
221
222 for i in 0..tabs.tab_count() {
224 if let Some(tab_rect) = tabs.tab_bounds(i, &abs_layout) {
225 let tab_right = tab_rect.x + tab_rect.width;
227 let bar_right = tab_row.x + tab_row.width;
228 if tab_right < tab_row.x || tab_rect.x > bar_right {
229 continue;
230 }
231
232 let tab_color = tabs.tab_background_color(i);
233
234 commands.push(DrawCommand::Quad(
236 QuadCommand::rounded(
237 Vec2::new(tab_rect.x, tab_rect.y),
238 Vec2::new(tab_rect.width, tab_rect.height),
239 tab_color,
240 4.0,
241 ctx.parent_z_index,
242 )
243 .with_clip(tab_clip),
244 ));
245
246 if let Some(label) = tabs.tab_label(i) {
248 let request_id = ctx.text_pipeline.request_shape(
249 label.to_string(),
250 0,
251 tabs.theme.tab_font_size,
252 None,
253 );
254
255 if let Some(shaped) = ctx.text_pipeline.get_completed(request_id) {
256 let text_height = shaped.bounds().1;
257 let text_x = tab_rect.x + DEFAULT_TAB_PADDING;
258 let text_y = tab_rect.y + (tab_rect.height - text_height) * 0.5;
259
260 commands.push(DrawCommand::Text(
261 TextCommand::new(
262 Vec2::new(text_x, text_y),
263 shaped,
264 tabs.theme.tab_text_color,
265 ctx.parent_z_index.saturating_add(1),
266 )
267 .with_clip(tab_clip),
268 ));
269 }
270
271 if tabs.theme.closable
273 && let Some(close_rect) = tabs.close_button_bounds(i, &abs_layout)
274 {
275 commands.push(DrawCommand::Quad(
276 QuadCommand::rounded(
277 Vec2::new(close_rect.x, close_rect.y),
278 Vec2::new(close_rect.width, close_rect.height),
279 Color::rgba(1.0, 1.0, 1.0, 0.1),
280 close_rect.width / 2.0,
281 ctx.parent_z_index,
282 )
283 .with_clip(tab_clip),
284 ));
285
286 let x_request = ctx.text_pipeline.request_shape(
288 "×".to_string(),
289 0,
290 tabs.theme.tab_font_size * 0.9,
291 None,
292 );
293
294 if let Some(x_shaped) = ctx.text_pipeline.get_completed(x_request) {
295 let x_width = x_shaped.bounds().0;
296 let x_height = x_shaped.bounds().1;
297 let x_x = close_rect.x + (close_rect.width - x_width) * 0.5;
298 let x_y = close_rect.y + (close_rect.height - x_height) * 0.5;
299
300 commands.push(DrawCommand::Text(
301 TextCommand::new(
302 Vec2::new(x_x, x_y),
303 x_shaped,
304 tabs.theme.tab_text_color,
305 ctx.parent_z_index.saturating_add(2),
306 )
307 .with_clip(tab_clip),
308 ));
309 }
310 }
311 }
312 }
313 }
314
315 if tabs.should_show_scrollbar() {
317 let track = tabs.scrollbar_track_bounds(&abs_layout);
318 commands.push(DrawCommand::Quad(
319 QuadCommand::filled(
320 Vec2::new(track.x, track.y),
321 Vec2::new(track.width, track.height),
322 tabs.theme.scrollbar_theme.track_color,
323 ctx.parent_z_index.saturating_add(2),
324 )
325 .with_clip(ctx.clip_rect),
326 ));
327
328 let thumb = tabs.scrollbar_thumb_bounds(&abs_layout);
329 let thumb_color = tabs.scrollbar_thumb_color();
330 commands.push(DrawCommand::Quad(
331 QuadCommand::rounded(
332 Vec2::new(thumb.x, thumb.y),
333 Vec2::new(thumb.width, thumb.height),
334 thumb_color,
335 tabs.theme.scrollbar_theme.thumb_border_radius,
336 ctx.parent_z_index.saturating_add(3),
337 )
338 .with_clip(ctx.clip_rect),
339 ));
340 }
341
342 if tabs.should_show_arrows() {
344 let arrow_color = Color::from_rgba_u8(180, 180, 180, 200);
345 let arrow_size = tabs.theme.tab_font_size;
346 let arrow_row = tabs.tab_row_bounds(&abs_layout);
347
348 if tabs.tab_scroll_offset > 0.0 {
350 let arrow_request = ctx.text_pipeline.request_shape(
351 "\u{25C0}".to_string(), 0,
353 arrow_size,
354 None,
355 );
356 if let Some(shaped) = ctx.text_pipeline.get_completed(arrow_request) {
357 let arrow_h = shaped.bounds().1;
358 let ax = arrow_row.x + 2.0;
359 let ay = arrow_row.y + (arrow_row.height - arrow_h) * 0.5;
360 commands.push(DrawCommand::Text(
361 TextCommand::new(
362 Vec2::new(ax, ay),
363 shaped,
364 arrow_color,
365 ctx.parent_z_index.saturating_add(3),
366 )
367 .with_clip(ctx.clip_rect),
368 ));
369 }
370 }
371
372 let max_offset = tabs.max_tab_scroll_offset(abs_layout.width);
374 if tabs.tab_scroll_offset < max_offset {
375 let arrow_request = ctx.text_pipeline.request_shape(
376 "\u{25B6}".to_string(), 0,
378 arrow_size,
379 None,
380 );
381 if let Some(shaped) = ctx.text_pipeline.get_completed(arrow_request) {
382 let arrow_w = shaped.bounds().0;
383 let arrow_h = shaped.bounds().1;
384 let ax = arrow_row.x + arrow_row.width - arrow_w - 2.0;
385 let ay = arrow_row.y + (arrow_row.height - arrow_h) * 0.5;
386 commands.push(DrawCommand::Text(
387 TextCommand::new(
388 Vec2::new(ax, ay),
389 shaped,
390 arrow_color,
391 ctx.parent_z_index.saturating_add(3),
392 )
393 .with_clip(ctx.clip_rect),
394 ));
395 }
396 }
397 }
398
399 if let Some(indicator_bounds) = tabs.drop_indicator_bounds(&abs_layout) {
401 let indicator_color = Color::from_rgba_u8(100, 150, 255, 200);
402 commands.push(DrawCommand::Quad(
403 QuadCommand::filled(
404 Vec2::new(indicator_bounds.x, indicator_bounds.y),
405 Vec2::new(indicator_bounds.width, indicator_bounds.height),
406 indicator_color,
407 ctx.parent_z_index.saturating_add(3),
408 )
409 .with_clip(ctx.clip_rect),
410 ));
411 }
412
413 if let Some(dragging_index) = tabs.drag.dragging_tab_index
415 && let Some(cursor_pos) = tabs.drag.drag_cursor_pos
416 {
417 let ghost_label = tabs.tab_label(dragging_index).unwrap_or("");
418
419 let char_width = tabs.theme.tab_font_size * CHAR_WIDTH_FACTOR;
420 let text_width = ghost_label.len() as f32 * char_width;
421 let close_width = if tabs.theme.closable {
422 DEFAULT_CLOSE_BUTTON_SIZE + CLOSE_BUTTON_MARGIN
423 } else {
424 0.0
425 };
426 let tab_width = text_width + DEFAULT_TAB_PADDING * 2.0 + close_width;
427
428 let ghost_pos = cursor_pos - Vec2::new(tab_width / 2.0, tabs.theme.tab_bar_height / 2.0);
429 let ghost_size = Vec2::new(tab_width, tabs.theme.tab_bar_height);
430 let ghost_color = Color::from_rgba_u8(80, 100, 140, 180);
431
432 commands.push(DrawCommand::Quad(
433 QuadCommand::rounded(
434 ghost_pos,
435 ghost_size,
436 ghost_color,
437 4.0,
438 ctx.parent_z_index.saturating_add(3),
439 )
440 .with_clip(ctx.clip_rect),
441 ));
442
443 let request_id = ctx.text_pipeline.request_shape(
445 ghost_label.to_string(),
446 0,
447 tabs.theme.tab_font_size,
448 None,
449 );
450
451 if let Some(shaped) = ctx.text_pipeline.get_completed(request_id) {
452 let text_height = shaped.bounds().1;
453 let text_x = ghost_pos.x + DEFAULT_TAB_PADDING;
454 let text_y = ghost_pos.y + (tabs.theme.tab_bar_height - text_height) * 0.5;
455 let ghost_text_color = Color::from_rgba_u8(200, 200, 200, 180);
456
457 commands.push(DrawCommand::Text(
458 TextCommand::new(
459 Vec2::new(text_x, text_y),
460 shaped,
461 ghost_text_color,
462 ctx.parent_z_index.saturating_add(4),
463 )
464 .with_clip(ctx.clip_rect),
465 ));
466 }
467 }
468
469 commands
470}
471
472pub fn dock_tabs_traversal(widget: &dyn Any) -> TraversalBehavior {
478 let tabs = widget.downcast_ref::<DockTabs>().unwrap();
479 TraversalBehavior::OnlyChild(tabs.active_tab)
480}
481
482pub fn dock_tabs_overflow(_widget: &dyn Any) -> WidgetOverflow {
487 WidgetOverflow {
488 overflow_x: Overflow::Hidden,
489 overflow_y: Overflow::Hidden,
490 }
491}