1mod a11y;
23mod activity_bar;
24mod context_menu;
25mod drag;
26mod geometry;
27mod model;
28mod panel;
29mod resize_handle;
30mod state;
31#[cfg(test)]
32mod tests;
33
34pub use activity_bar::{DockAction, DockActionId, DockActionPlacement, DockRail, DockRailSlot};
35pub use geometry::{CornerOwners, DockCorner, DockSide, DockingRects, SideLayout, SideRects};
36pub use model::{
37 DockIconFactory, DockLoc, DockOpenLocation, DockOpenMode, DockPolicy, DockRailItemSize,
38 DockTabDisplay, DockTabId, DockWidgetId, DockingModel, TabPresentation,
39};
40pub use panel::{DockContentFactory, DockWidget};
41pub use state::{DockLayoutState, DockSideState, DockTabState};
42
43use std::cell::{Cell, RefCell};
44use std::collections::HashMap;
45use std::rc::Rc;
46
47use teksilo_canvas::{Point, Rect, Size, SizeProposal};
48use teksilo_core::accessibility::AccessNodeBuilder;
49use teksilo_core::binding::BindingLevel;
50use teksilo_core::build_context::BuildContext;
51use teksilo_core::widget::{LayoutContext, LayoutResponse, Widget, WidgetPlacement};
52use teksilo_core::widget_id::WidgetId;
53use teksilo_tokens::SurfaceRole;
54
55use crate::primitives::RectWidget;
56
57use activity_bar::DockActivityBar;
58use geometry::compute_rects;
59use panel::{DockContentRegistry, DockSidePanel};
60use resize_handle::{DockResizeHandle, DockResizeHandleConfig};
61
62const COLLAPSED_EPS: f32 = 0.01;
66const DOCK_GUTTER: f32 = 6.0;
68
69pub struct DockingLayout {
79 model: DockingModel,
80 registry: Rc<RefCell<DockContentRegistry>>,
81 center: Option<Box<dyn Widget>>,
82 center_id: Option<WidgetId>,
83 container_bounds: Rc<Cell<Rect>>,
84 progress: HashMap<DockSide, teksilo_core::signal::Signal<f32>>,
85 rails: HashMap<DockSide, DockRail>,
87 side_panel_ids: Rc<RefCell<HashMap<DockSide, WidgetId>>>,
94 ordered: Vec<WidgetId>,
97}
98
99impl std::fmt::Debug for DockingLayout {
100 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101 f.debug_struct("DockingLayout").finish()
102 }
103}
104
105impl DockingLayout {
106 pub fn new(model: DockingModel) -> Self {
108 Self {
109 model,
110 registry: Rc::new(RefCell::new(DockContentRegistry::default())),
111 center: None,
112 center_id: None,
113 container_bounds: Rc::new(Cell::new(Rect::ZERO)),
114 progress: HashMap::new(),
115 rails: HashMap::new(),
116 side_panel_ids: Rc::new(RefCell::new(HashMap::new())),
117 ordered: Vec::new(),
118 }
119 }
120
121 pub fn rail(mut self, rail: DockRail) -> Self {
125 self.rails.insert(rail.side(), rail);
126 self
127 }
128
129 pub fn center(mut self, widget: impl Widget + 'static) -> Self {
131 self.center = Some(Box::new(widget));
132 self
133 }
134
135 pub fn policy(self, policy: DockPolicy) -> Self {
138 self.model.set_policy(policy);
139 self
140 }
141
142 pub fn disable_side(self, side: DockSide) -> Self {
145 self.model.set_side_enabled(side, false);
146 self
147 }
148
149 pub fn center_id(mut self, id: WidgetId) -> Self {
151 self.center_id = Some(id);
152 self
153 }
154
155 pub fn dock(self, dock: DockWidget) -> Self {
159 let (id, meta, factory) = dock.into_parts();
160 self.model.register_meta(id, meta);
161 self.registry.borrow_mut().insert(id, factory);
162 self
163 }
164}
165
166const SIDES_ORDER: [DockSide; 4] = [
167 DockSide::Leading,
168 DockSide::Trailing,
169 DockSide::Top,
170 DockSide::Bottom,
171];
172
173impl Widget for DockingLayout {
174 fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
175 let self_id = ctx.self_id();
176
177 self.model
179 .version()
180 .bind_to(self_id, ctx.binding_registry(), BindingLevel::Rebuild);
181 self.model.geometry_version().bind_to(
182 self_id,
183 ctx.binding_registry(),
184 BindingLevel::Relayout,
185 );
186
187 let prior = std::mem::take(&mut self.ordered);
201 let preserved_center = prior.first().copied();
202 for &old_side in prior.iter().skip(1) {
203 ctx.destroy_subtree(old_side);
204 }
205
206 let center = if let Some(c) = preserved_center {
208 c
209 } else {
210 let inner = if let Some(id) = self.center_id {
211 id
212 } else if let Some(w) = self.center.take() {
213 ctx.add_boxed(w)
214 } else {
215 ctx.add(RectWidget::new().background(SurfaceRole::Content))
216 };
217 ctx.add(crate::primitives::Expand::new().child_id(inner))
218 };
219
220 let mut ordered = vec![center];
221 let anim = ctx.animate().collapse().standard();
222
223 self.side_panel_ids.borrow_mut().clear();
227
228 for side in SIDES_ORDER {
229 if !self.model.is_side_enabled(side) {
234 let blank = || RectWidget::new().background(SurfaceRole::Transparent);
235 ordered.push(ctx.add(blank()));
236 ordered.push(ctx.add(blank()));
237 ordered.push(ctx.add(blank()));
238 continue;
239 }
240
241 let visible = self.model.side_visible_signal(side);
242 let progress = ctx.animated_signal(if visible.get() { 1.0 } else { 0.0 });
243 progress.bind_to(self_id, ctx.binding_registry(), BindingLevel::Relayout);
244 self.progress.insert(side, progress.clone());
245
246 self.model.rail_size_signal(side).bind_to(
250 self_id,
251 ctx.binding_registry(),
252 BindingLevel::Relayout,
253 );
254
255 {
257 let spec = anim.clone();
258 let p = progress.clone();
259 ctx.effect(&visible, move |&v| {
260 spec.to_or_snap(&p, if v { 1.0 } else { 0.0 });
261 });
262 }
263
264 let config = self
276 .rails
277 .get(&side)
278 .cloned()
279 .unwrap_or_else(|| DockRail::new(side));
280 let panel = ctx.add(DockSidePanel::new(
281 side,
282 self.model.clone(),
283 self.registry.clone(),
284 config.clone(),
285 ));
286 self.side_panel_ids.borrow_mut().insert(side, panel);
289 ctx.visible_when(panel, progress.map(|p| *p > COLLAPSED_EPS));
294 let content = ctx.add(SideClipPane {
295 side,
296 model: self.model.clone(),
297 child: panel,
298 });
299
300 let rail = if self.model.side_has_rail(side) {
302 ctx.add(DockActivityBar::new(
303 side,
304 self.model.clone(),
305 config,
306 self.side_panel_ids.clone(),
307 ))
308 } else {
309 ctx.add(RectWidget::new().background(SurfaceRole::Transparent))
310 };
311
312 let handle = ctx.add(DockResizeHandle::new(DockResizeHandleConfig {
314 side,
315 model: self.model.clone(),
316 enabled: true,
317 is_rtl: false,
318 container_bounds: self.container_bounds.clone(),
319 }));
320 ctx.enabled_when(handle, visible.clone());
321
322 ordered.push(content);
323 ordered.push(rail);
324 ordered.push(handle);
325 }
326
327 self.ordered = ordered.clone();
328 ordered
329 }
330
331 fn layout_response(&self, proposal: SizeProposal, ctx: &LayoutContext) -> LayoutResponse {
332 let mut min_w = 0.0_f32;
334 let mut min_h = 0.0_f32;
335 if let Some(¢er) = self.ordered.first()
336 && let Some(c) = ctx.child_size(
337 center,
338 SizeProposal {
339 width: None,
340 height: None,
341 },
342 )
343 {
344 min_w += c.width.min(80.0);
345 min_h += c.height.min(80.0);
346 }
347 for side in SIDES_ORDER {
348 if self.model.is_side_enabled(side) && self.model.is_side_visible(side) {
349 let extent = self.model.side_min_size(side)
350 + DOCK_GUTTER
351 + self.model.side_rail_thickness(side);
352 if side.is_horizontal_axis() {
353 min_w += extent;
354 } else {
355 min_h += extent;
356 }
357 }
358 }
359 LayoutResponse::shrinkable(
360 proposal.resolve(min_w, min_h),
361 teksilo_canvas::Size::new(min_w, min_h),
362 1.0,
363 )
364 }
365
366 fn place_children(
367 &self,
368 bounds: Rect,
369 _proposal: SizeProposal,
370 children: &mut [WidgetPlacement],
371 ctx: &LayoutContext,
372 ) {
373 self.container_bounds.set(bounds);
374 let rtl = ctx.is_rtl();
375
376 let side_layout = |side: DockSide| -> SideLayout {
377 if !self.model.is_side_enabled(side) {
380 return SideLayout {
381 size: 0.0,
382 visible_progress: 0.0,
383 gutter: DOCK_GUTTER,
384 min_size: 0.0,
385 rail_thickness: 0.0,
386 has_rail: false,
387 };
388 }
389 let p = self.progress.get(&side).map(|s| s.get()).unwrap_or(
390 if self.model.is_side_visible(side) {
391 1.0
392 } else {
393 0.0
394 },
395 );
396 let rail_thickness = if self.model.side_has_rail(side) {
399 let mode = self.model.side_rail_size(side);
400 self.rails
401 .get(&side)
402 .map(|r| r.effective_thickness(mode))
403 .unwrap_or_else(|| DockRail::new(side).effective_thickness(mode))
404 } else {
405 0.0
406 };
407 SideLayout {
408 size: self.model.side_size(side),
409 visible_progress: p,
410 gutter: DOCK_GUTTER,
411 min_size: self.model.side_min_size(side),
412 rail_thickness,
413 has_rail: self.model.side_has_rail(side),
414 }
415 };
416
417 let (lead_in, trail_in) = if rtl {
419 (
420 side_layout(DockSide::Trailing),
421 side_layout(DockSide::Leading),
422 )
423 } else {
424 (
425 side_layout(DockSide::Leading),
426 side_layout(DockSide::Trailing),
427 )
428 };
429 let rects = compute_rects(
430 bounds,
431 lead_in,
432 trail_in,
433 side_layout(DockSide::Top),
434 side_layout(DockSide::Bottom),
435 self.model.corners(),
436 rtl,
437 );
438 let leading = if rtl { rects.trailing } else { rects.leading };
439 let trailing = if rtl { rects.leading } else { rects.trailing };
440
441 let place = |children: &mut [WidgetPlacement], idx: usize, rect: Rect| {
445 if let Some(c) = children.get_mut(idx) {
446 c.origin = rect.origin();
447 c.size = rect.size();
448 }
449 };
450 place(children, 0, rects.center);
451 let side_rects = [
452 (leading.content, leading.rail, leading.handle),
453 (trailing.content, trailing.rail, trailing.handle),
454 (rects.top.content, rects.top.rail, rects.top.handle),
455 (rects.bottom.content, rects.bottom.rail, rects.bottom.handle),
456 ];
457 for (i, (content, rail, handle)) in side_rects.into_iter().enumerate() {
458 let base = 1 + i * 3;
459 place(children, base, content);
460 place(children, base + 1, rail);
461 place(children, base + 2, handle);
462 }
463 }
464
465 fn clips_children(&self) -> bool {
466 true
467 }
468
469 fn preserves_children_on_rebuild(&self) -> bool {
475 true
476 }
477
478 fn accessibility(&self, builder: &mut AccessNodeBuilder) {
479 builder.set_role(teksilo_core::accesskit::Role::GenericContainer);
480 }
481
482 fn children(&self) -> Vec<WidgetId> {
483 self.ordered.clone()
484 }
485}
486
487struct SideClipPane {
493 side: DockSide,
494 model: DockingModel,
495 child: WidgetId,
496}
497
498impl std::fmt::Debug for SideClipPane {
499 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
500 f.debug_struct("SideClipPane")
501 .field("side", &self.side)
502 .finish()
503 }
504}
505
506impl Widget for SideClipPane {
507 fn layout_response(&self, proposal: SizeProposal, ctx: &LayoutContext) -> LayoutResponse {
508 let full = self.model.side_size(self.side).max(0.0);
514 let full_proposal = if self.side.is_horizontal_axis() {
515 SizeProposal {
516 width: Some(full.max(proposal.width.unwrap_or(0.0))),
517 height: proposal.height,
518 }
519 } else {
520 SizeProposal {
521 width: proposal.width,
522 height: Some(full.max(proposal.height.unwrap_or(0.0))),
523 }
524 };
525 let _ = ctx.child_size(self.child, full_proposal);
526 proposal
527 .resolve(
528 proposal.width.unwrap_or(0.0),
529 proposal.height.unwrap_or(0.0),
530 )
531 .into()
532 }
533
534 fn place_children(
535 &self,
536 bounds: Rect,
537 _proposal: SizeProposal,
538 children: &mut [WidgetPlacement],
539 _ctx: &LayoutContext,
540 ) {
541 let full = self.model.side_size(self.side).max(0.0);
545 let (size, origin) = match self.side {
546 DockSide::Leading => {
547 let w = full.max(bounds.width);
548 (
549 Size::new(w, bounds.height),
550 Point::new(bounds.x + bounds.width - w, bounds.y),
551 )
552 }
553 DockSide::Trailing => {
554 let w = full.max(bounds.width);
555 (Size::new(w, bounds.height), Point::new(bounds.x, bounds.y))
556 }
557 DockSide::Top => {
558 let h = full.max(bounds.height);
559 (
560 Size::new(bounds.width, h),
561 Point::new(bounds.x, bounds.y + bounds.height - h),
562 )
563 }
564 DockSide::Bottom => {
565 let h = full.max(bounds.height);
566 (Size::new(bounds.width, h), Point::new(bounds.x, bounds.y))
567 }
568 };
569 for child in children.iter_mut() {
570 child.origin = origin;
571 child.size = size;
572 }
573 }
574
575 fn clips_children(&self) -> bool {
576 true
577 }
578
579 fn children(&self) -> Vec<WidgetId> {
580 vec![self.child]
581 }
582}