use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use teksilo_canvas::{Rect, SizeProposal};
use teksilo_core::WidgetBuilder;
use teksilo_core::accessibility::AccessNodeBuilder;
use teksilo_core::binding::BindingLevel;
use teksilo_core::build_context::BuildContext;
use teksilo_core::signal::Signal;
use teksilo_core::widget::{LayoutContext, LayoutResponse, Widget, WidgetPlacement};
use teksilo_core::widget_builder::HandlerSet;
use teksilo_core::widget_id::WidgetId;
use teksilo_core::{DragPayload, DropFeedback};
use teksilo_i18n::{LocalizedString, lit};
use teksilo_tokens::{SurfaceRole, TextRole, TextStyleRole};
use crate::DropRegion;
use crate::accordion::{
ACCORDION_FILL_HEADER_EXTENT, ACCORDION_HEADER_PADDING_HORIZONTAL, Accordion,
AccordionOrientation,
};
use crate::drop_target::DropTarget;
use crate::icon_button::{IconButton, IconButtonSize};
use crate::popover_widget::PopoverIconButton;
use crate::primitives::{
Center, Divider, Expand, HStack, IconWidget, MinSize, Padding, RectWidget, Spacer, TextWidget,
VStack,
};
use crate::splitter::Splitter;
use crate::toolbar::{Toolbar, ToolbarItem, ToolbarOrientation};
use teksilo_core::overlay::OverlayPlacement;
use super::context_menu::{
DockMenuKind, activity_context_menu, background_menu, dock_has_options, dock_options_menu,
};
use super::drag::{DockDragData, dropped_dock_tab, dropped_dock_widget};
use super::geometry::DockSide;
use super::model::{
DockHeaderActionsFactory, DockIconFactory, DockOpenLocation, DockTabId, DockTabView,
DockWidgetId, DockWidgetMeta, DockingModel, side_orientation,
};
pub type DockContentFactory = Rc<dyn Fn(DockWidgetId) -> Box<dyn Widget>>;
pub struct DockWidget {
id: DockWidgetId,
title: LocalizedString,
icon: Option<DockIconFactory>,
default: DockOpenLocation,
factory: DockContentFactory,
header_actions: Option<DockHeaderActionsFactory>,
show_header: bool,
}
impl DockWidget {
pub fn new<W: Widget + 'static>(
id: DockWidgetId,
title: impl Into<LocalizedString>,
factory: impl Fn(DockWidgetId) -> W + 'static,
) -> Self {
Self {
id,
title: title.into(),
icon: None,
default: DockOpenLocation::side(DockSide::Leading),
factory: Rc::new(move |i| Box::new(factory(i)) as Box<dyn Widget>),
header_actions: None,
show_header: false,
}
}
pub fn icon(mut self, f: impl Fn() -> IconWidget + 'static) -> Self {
self.icon = Some(Rc::new(f));
self
}
pub fn header_actions(
mut self,
f: impl Fn(DockWidgetId) -> Vec<ToolbarItem> + 'static,
) -> Self {
self.header_actions = Some(Rc::new(f));
self
}
pub fn show_header(mut self, show: bool) -> Self {
self.show_header = show;
self
}
pub fn default_location(mut self, loc: DockOpenLocation) -> Self {
self.default = loc;
self
}
pub(crate) fn id(&self) -> DockWidgetId {
self.id
}
pub(crate) fn into_parts(self) -> (DockWidgetId, DockWidgetMeta, DockContentFactory) {
(
self.id,
DockWidgetMeta {
title: self.title,
icon: self.icon,
min_size: None,
default: self.default,
header_actions: self.header_actions,
show_header: self.show_header,
},
self.factory,
)
}
}
#[derive(Default)]
pub(crate) struct DockContentRegistry {
factories: HashMap<DockWidgetId, DockContentFactory>,
}
impl std::fmt::Debug for DockContentRegistry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DockContentRegistry")
.field("factories", &self.factories.len())
.finish()
}
}
impl DockContentRegistry {
pub(crate) fn insert(&mut self, id: DockWidgetId, factory: DockContentFactory) {
self.factories.insert(id, factory);
}
pub(crate) fn build(&self, id: DockWidgetId) -> Option<Box<dyn Widget>> {
self.factories.get(&id).map(|f| f(id))
}
}
pub(crate) type DockContent = Rc<RefCell<DockContentRegistry>>;
const DOCK_TAB_KIND: &str = "__dock_tab__";
#[derive(Clone, Copy)]
struct DockTabPayload {
tab_id: DockTabId,
}
#[derive(Debug)]
pub(crate) struct DockSidePanel {
side: DockSide,
model: DockingModel,
content: DockContent,
config: super::DockRail,
root: Option<WidgetId>,
}
impl DockSidePanel {
pub(crate) fn new(
side: DockSide,
model: DockingModel,
content: DockContent,
config: super::DockRail,
) -> Self {
Self {
side,
model,
content,
config,
root: None,
}
}
fn compose_bar_slots(
&self,
ctx: &mut BuildContext,
needs_hamburger: bool,
) -> (Option<WidgetId>, Option<WidgetId>) {
let leading = self
.config
.leading_slot
.as_ref()
.map(|f| ctx.add_boxed((f)()));
let mut trailing: Vec<WidgetId> = Vec::new();
if let Some(f) = self.config.trailing_slot.as_ref() {
trailing.push(ctx.add_boxed((f)()));
}
if needs_hamburger {
let m = self.model.clone();
let hb_side = self.side;
trailing.push(
ctx.add(
PopoverIconButton::new(IconButton::menu().tooltip(lit!("Hidden activities")))
.content(background_menu(&m, hb_side, DockMenuKind::Strip))
.placement(OverlayPlacement::BelowPreferred),
),
);
}
let trailing = match trailing.len() {
0 => None,
1 => Some(trailing[0]),
_ => {
let mut row = HStack::new().spacing(2.0);
for id in &trailing {
row = row.add_child(*id);
}
Some(ctx.add(row))
}
};
(leading, trailing)
}
}
fn empty_side_drop_target(
ctx: &mut BuildContext,
model: &DockingModel,
side: DockSide,
) -> WidgetId {
let text = ctx.add(
TextWidget::new(lit!("Drop a panel here"))
.style(TextStyleRole::Body)
.color(TextRole::Secondary),
);
let label = ctx.add(Center::new().child_id(text));
let m = model.clone();
ctx.add(
DropTarget::new()
.child_id(label)
.accept_when(|p| dropped_dock_tab(p).is_some() || dropped_dock_widget(p).is_some())
.on_drop(move |p, _pos, ctx| {
if !m.is_side_enabled(side) {
return false;
}
if let Some(tab_id) = dropped_dock_tab(&p) {
m.move_tab(tab_id, side, 0);
m.set_side_visible(side, true);
ctx.request_accessibility_update();
true
} else if let Some(dock_id) = dropped_dock_widget(&p) {
m.move_dock(dock_id, DockOpenLocation::side(side));
m.set_side_visible(side, true);
ctx.request_accessibility_update();
true
} else {
false
}
}),
)
}
impl Widget for DockSidePanel {
fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
use crate::tab_widget::{
TabBarVisibility, TabDisplayMode, TabHandle, TabId, TabInfo, TabWidget,
};
use std::any::Any;
use std::num::NonZeroU64;
use teksilo_data::ListModel;
let all_tabs = self.model.side_tabs(self.side);
if all_tabs.is_empty() {
let drop = empty_side_drop_target(ctx, &self.model, self.side);
let (leading, trailing) = self.compose_bar_slots(ctx, false);
if leading.is_none() && trailing.is_none() {
self.root = Some(drop);
return vec![drop];
}
let mut bar = HStack::new().spacing(2.0);
if let Some(id) = leading {
bar = bar.add_child(id);
}
bar = bar.add_child(ctx.add(Spacer::new()));
if let Some(id) = trailing {
bar = bar.add_child(id);
}
let bar = ctx.add(bar);
let body = ctx.add(Expand::new().child_id(drop));
let root = ctx.add(VStack::new().add_child(bar).add_child(body));
self.root = Some(root);
return vec![root];
}
let self_id = ctx.self_id();
self.model.tab_display_signal(self.side).bind_to(
self_id,
ctx.binding_registry(),
BindingLevel::Rebuild,
);
let display = match self.model.side_tab_display(self.side) {
super::model::DockTabDisplay::Icon => TabDisplayMode::Icon,
super::model::DockTabDisplay::IconText => TabDisplayMode::IconText,
super::model::DockTabDisplay::Text => TabDisplayMode::Text,
};
let to_tab_id = |t: &DockTabView| {
TabId::from_raw(NonZeroU64::new(t.id.raw()).unwrap_or(NonZeroU64::MIN))
};
let all_tab_ids: Vec<TabId> = all_tabs.iter().map(&to_tab_id).collect();
let model_indices: Vec<usize> = all_tabs
.iter()
.enumerate()
.filter(|(_, t)| !t.hidden)
.map(|(i, _)| i)
.collect();
let presentation = self.model.side_presentation(self.side);
if model_indices.is_empty() && presentation == super::model::TabPresentation::Rail {
let empty = ctx.add(RectWidget::new().background(SurfaceRole::Transparent));
self.root = Some(empty);
return vec![empty];
}
let dock_selected = self.model.side_selected_tab_signal(self.side);
let initial = all_tab_ids
.get(dock_selected.get().min(all_tab_ids.len().saturating_sub(1)))
.copied();
let tw_selected: Signal<Option<TabId>> = ctx.signal(initial);
{
let model = self.model.clone();
let side = self.side;
let tw = tw_selected.clone();
ctx.effect(&dock_selected, move |&idx| {
let target = model.tab_id_at(side, idx).map(|id| {
TabId::from_raw(NonZeroU64::new(id.raw()).unwrap_or(NonZeroU64::MIN))
});
if tw.get() != target {
tw.set(target);
}
});
}
{
let model = self.model.clone();
let side = self.side;
ctx.effect(&tw_selected, move |maybe| {
if let Some(tid) = maybe {
model.select_tab_by_id(side, DockTabId::from_raw(tid.raw().get()));
}
});
}
let bar_visibility = match presentation {
super::model::TabPresentation::Rail => TabBarVisibility::Never,
super::model::TabPresentation::Strip => TabBarVisibility::Always,
};
let needs_hamburger = model_indices.is_empty();
let mut handles: Vec<TabHandle> = Vec::with_capacity(model_indices.len());
for &model_i in &model_indices {
let tab = &all_tabs[model_i];
let label = self.model.activity_label(tab);
let icon_factory = self.model.activity_icon(tab);
let mut info = TabInfo::new().closable(false).title(label.clone());
if let Some(icf) = icon_factory {
info = info.icon(move || (icf)());
}
{
let m = self.model.clone();
let menu_side = self.side;
let tid = tab.id;
info = info.context_menu(move |_pos, _ctx| {
Some(Box::new(activity_context_menu(
&m,
menu_side,
tid,
DockMenuKind::Strip,
)))
});
}
handles.push(TabHandle::dynamic(
to_tab_id(tab),
DOCK_TAB_KIND,
info,
DockTabPayload { tab_id: tab.id },
));
}
let list: ListModel<TabHandle> = ListModel::from_vec(handles);
let side = self.side;
let factory_model = self.model.clone();
let factory_content = self.content.clone();
let ext_indices = model_indices.clone();
let ext_model = self.model.clone();
let after_last_visible = model_indices
.last()
.map(|&i| i + 1)
.unwrap_or(all_tab_ids.len());
let policy = self.model.policy();
let mut tw = TabWidget::new(tw_selected)
.bar_visibility(bar_visibility)
.compact_bar()
.tab_sizing(crate::tab_widget::TabSizing::Independent)
.tab_display(display)
.min_tab_width(40.0)
.dynamic_model(list)
.dynamic_tab::<DockTabPayload>(DOCK_TAB_KIND, move |_handle, payload| {
match factory_model.tab_view_by_id(payload.tab_id) {
Some((tside, view)) => Box::new(DockTabContentWidget::new(
tside,
view,
factory_model.clone(),
factory_content.clone(),
)) as Box<dyn Widget>,
None => Box::new(RectWidget::new().background(SurfaceRole::Transparent)),
}
})
.on_external_drop(move |payload, idx, ctx| {
if !ext_model.is_side_enabled(side) {
return false;
}
let at = ext_indices.get(idx).copied().unwrap_or(after_last_visible);
if let Some(tab_id) = dropped_dock_tab(payload) {
ext_model.move_tab(tab_id, side, at);
ctx.request_accessibility_update();
true
} else if let Some(dock_id) = dropped_dock_widget(payload) {
ext_model.promote_to_tab(dock_id, side, at);
ctx.request_accessibility_update();
true
} else {
false
}
});
if policy.allow_activity_drag {
let reorder_model = self.model.clone();
let recv_model = self.model.clone();
let reorder_indices = model_indices.clone();
let recv_indices = model_indices.clone();
tw = tw
.reorderable(true)
.accept_external_tabs(true)
.on_reorder(move |tid, dest, _ctx| {
let at = reorder_indices
.get(dest)
.copied()
.unwrap_or(after_last_visible);
reorder_model.move_tab(DockTabId::from_raw(tid.raw().get()), side, at);
})
.on_tab_received(move |handle, idx, ctx| {
if let Some(p) =
(handle.payload.as_ref() as &dyn Any).downcast_ref::<DockTabPayload>()
{
let at = recv_indices.get(idx).copied().unwrap_or(after_last_visible);
recv_model.move_tab(p.tab_id, side, at);
ctx.request_accessibility_update();
}
})
.on_transfer_out(|_tid, _ctx| {});
}
let (leading_slot, trailing_slot) = self.compose_bar_slots(ctx, needs_hamburger);
if let Some(id) = leading_slot {
tw = tw.bar_leading_slot_id(id);
}
if let Some(id) = trailing_slot {
tw = tw.bar_trailing_slot_id(id);
}
let root = ctx.add(tw);
self.root = Some(root);
let drop_model = self.model.clone();
let drop_side = self.side;
ctx.apply_self_handlers(
HandlerSet::new()
.on_drag_hover(move |_payload, _pos, _ctx| {
DropFeedback::NoFeedback
})
.on_drop(move |payload, _pos, ctx| {
if !drop_model.is_side_enabled(drop_side) {
return false;
}
if let Some(tab_id) = dropped_dock_tab(&payload) {
let at = drop_model.side_append_index(drop_side);
drop_model.move_tab(tab_id, drop_side, at);
ctx.request_accessibility_update();
true
} else if let Some(dock_id) = dropped_dock_widget(&payload) {
drop_model.move_dock(dock_id, DockOpenLocation::side(drop_side));
ctx.request_accessibility_update();
true
} else {
false
}
}),
);
vec![root]
}
fn layout_response(&self, proposal: SizeProposal, ctx: &LayoutContext) -> LayoutResponse {
self.root
.and_then(|id| ctx.child_size(id, proposal))
.unwrap_or_else(|| proposal.resolve(0.0, 0.0))
.into()
}
fn place_children(
&self,
bounds: Rect,
_proposal: SizeProposal,
children: &mut [WidgetPlacement],
_ctx: &LayoutContext,
) {
for child in children.iter_mut() {
child.origin = bounds.origin();
child.size = bounds.size();
}
}
fn accessibility(&self, builder: &mut AccessNodeBuilder) {
use teksilo_core::accesskit::Role;
builder.set_role(Role::Complementary);
builder.set_name(super::a11y::side_label(self.side).resolve_now());
}
fn children(&self) -> Vec<WidgetId> {
self.root.into_iter().collect()
}
}
struct DockTabContentWidget {
side: DockSide,
tab: DockTabView,
model: DockingModel,
content: DockContent,
root: Option<WidgetId>,
}
impl std::fmt::Debug for DockTabContentWidget {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DockTabContentWidget")
.field("side", &self.side)
.field("panes", &self.tab.panes.len())
.finish()
}
}
impl DockTabContentWidget {
fn new(side: DockSide, tab: DockTabView, model: DockingModel, content: DockContent) -> Self {
Self {
side,
tab,
model,
content,
root: None,
}
}
fn build_dock_content(&self, ctx: &mut BuildContext, dock: DockWidgetId) -> WidgetId {
match self.content.borrow().build(dock) {
Some(w) => ctx.add_boxed(w),
None => ctx.add(TextWidget::new(lit!("(missing content)"))),
}
}
}
impl Widget for DockTabContentWidget {
fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
let tab_idx = self
.model
.side_tabs(self.side)
.iter()
.position(|t| t.id == self.tab.id)
.unwrap_or(0);
let root = if self.tab.panes.len() <= 1 {
match self.tab.panes.first() {
Some(dock) => {
let inner = self.build_pane_inner(ctx, *dock, 0, None);
ctx.add(DockPanePane::new(
self.side,
tab_idx,
0,
self.model.clone(),
inner,
))
}
None => ctx.add(RectWidget::new().background(SurfaceRole::Transparent)),
}
} else {
let splitter_model = self.tab.splitter.clone();
let mut splitter = Splitter::new(splitter_model.clone());
for (pane_idx, dock) in self.tab.panes.iter().enumerate() {
let inner = self.build_pane_inner(ctx, *dock, pane_idx, Some(&splitter_model));
let pane_widget = ctx.add(DockPanePane::new(
self.side,
tab_idx,
pane_idx,
self.model.clone(),
inner,
));
splitter = splitter.pane_id(pane_widget);
}
ctx.add(splitter)
};
self.root = Some(root);
vec![root]
}
fn layout_response(&self, proposal: SizeProposal, ctx: &LayoutContext) -> LayoutResponse {
self.root
.and_then(|id| ctx.child_size(id, proposal))
.unwrap_or_else(|| proposal.resolve(0.0, 0.0))
.into()
}
fn place_children(
&self,
bounds: Rect,
_proposal: SizeProposal,
children: &mut [WidgetPlacement],
_ctx: &LayoutContext,
) {
for child in children.iter_mut() {
child.origin = bounds.origin();
child.size = bounds.size();
}
}
fn children(&self) -> Vec<WidgetId> {
self.root.into_iter().collect()
}
}
impl DockTabContentWidget {
fn build_pane_inner(
&self,
ctx: &mut BuildContext,
dock: DockWidgetId,
pane_idx: usize,
splitter: Option<&crate::splitter::SplitterModel>,
) -> WidgetId {
let content = self.build_dock_content(ctx, dock);
let multi_pane = splitter.is_some();
let Some(splitter) = splitter else {
if !self.model.dock_show_header(dock) {
return content;
}
return self.build_bare_dock_header(ctx, dock, content);
};
let title = self.model.dock_title(dock).unwrap_or_else(|| lit!("Panel"));
let expanded = ctx.signal(!splitter.is_collapsed(pane_idx));
splitter.set_collapsed_size(pane_idx, crate::accordion::ACCORDION_FILL_COLLAPSED_EXTENT);
{
let sp = splitter.clone();
ctx.effect(&expanded, move |&e| {
sp.set_collapsed(pane_idx, !e);
});
}
let mut accordion = Accordion::new(title, expanded)
.orientation(
if side_orientation(self.side) == teksilo_tokens::Orientation::Vertical {
AccordionOrientation::Vertical
} else {
AccordionOrientation::Horizontal
},
)
.fill(true);
if let Some(trailing) = self.dock_header_trailing(ctx, dock, multi_pane) {
accordion = accordion.trailing_id(trailing);
}
if self.model.policy().allow_dock_drag {
accordion = accordion.on_header_drag(move |ctx| {
ctx.start_drag(content, DragPayload::typed(DockDragData { dock_id: dock }));
});
}
ctx.add(accordion.content_id(content))
}
fn dock_header_trailing(
&self,
ctx: &mut BuildContext,
dock: DockWidgetId,
multi_pane: bool,
) -> Option<WidgetId> {
let actions = self.model.dock_header_actions(dock);
let has_options = dock_has_options(&self.model, self.side, multi_pane);
if actions.is_none() && !has_options {
return None;
}
let vertical =
multi_pane && side_orientation(self.side) == teksilo_tokens::Orientation::Horizontal;
let title = self.model.dock_title(dock).unwrap_or_else(|| lit!("Panel"));
let toolbar_id = actions.map(|factory| {
let mut bar = Toolbar::new()
.orientation(if vertical {
ToolbarOrientation::Vertical
} else {
ToolbarOrientation::Horizontal
})
.compact(true)
.spacing(2.0)
.label(lit!(format!("{} actions", title.resolve_now())));
for item in factory(dock) {
bar = bar.item(item);
}
ctx.add(bar)
});
let options_id = has_options.then(|| {
let menu = dock_options_menu(&self.model, self.side, self.tab.id, dock, multi_pane);
ctx.add(
PopoverIconButton::new(IconButton::more().size(IconButtonSize::Compact))
.bare()
.content(menu)
.placement(OverlayPlacement::BelowPreferred)
.access_label(lit!(format!("More actions: {}", title.resolve_now()))),
)
});
let kids: Vec<WidgetId> = [toolbar_id, options_id].into_iter().flatten().collect();
match kids.as_slice() {
[] => None,
[only] => Some(*only),
_ => {
let cluster = if vertical {
let mut col = VStack::new().spacing(2.0);
for k in &kids {
col = col.add_child(*k);
}
ctx.add(col)
} else {
let mut row = HStack::new().spacing(2.0);
for k in &kids {
row = row.add_child(*k);
}
ctx.add(row)
};
Some(cluster)
}
}
}
fn build_bare_dock_header(
&self,
ctx: &mut BuildContext,
dock: DockWidgetId,
content: WidgetId,
) -> WidgetId {
let title = self.model.dock_title(dock).unwrap_or_else(|| lit!("Panel"));
let title_id = ctx.add(
TextWidget::new(title)
.style(TextStyleRole::BodyBold)
.color(TextRole::Primary)
.single_line()
.no_shrink(),
);
let spacer_id = ctx.add(Spacer::new());
let mut row = HStack::new()
.spacing(2.0)
.add_child(title_id)
.add_child(spacer_id);
if let Some(trailing) = self.dock_header_trailing(ctx, dock, false) {
row = row.add_child(trailing);
}
let row_id = ctx.add(row);
let padded =
ctx.add(Padding::symmetric(2.0, ACCORDION_HEADER_PADDING_HORIZONTAL).child_id(row_id));
let header = ctx.add(MinSize::new(0.0, ACCORDION_FILL_HEADER_EXTENT).child_id(padded));
let divider = ctx.add(Divider::horizontal());
ctx.add(
VStack::new()
.add_child(header)
.add_child(divider)
.child(Expand::new().flex(1.0).child_id(content)),
)
}
}
#[derive(Debug)]
pub(crate) struct DockPanePane {
side: DockSide,
tab_idx: usize,
pane_idx: usize,
model: DockingModel,
inner: WidgetId,
root: Option<WidgetId>,
}
impl DockPanePane {
pub(crate) fn new(
side: DockSide,
tab_idx: usize,
pane_idx: usize,
model: DockingModel,
inner: WidgetId,
) -> Self {
Self {
side,
tab_idx,
pane_idx,
model,
inner,
root: None,
}
}
}
impl Widget for DockPanePane {
fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
let side = self.side;
let tab_idx = self.tab_idx;
let pane_idx = self.pane_idx;
let split_model = self.model.clone();
let target = DropTarget::new()
.child_id(self.inner)
.zone_size_factor(0.2)
.region(DropRegion::Center, |z| z)
.region(DropRegion::Leading, |z| z)
.region(DropRegion::Trailing, |z| z)
.region(DropRegion::Top, |z| z)
.region(DropRegion::Bottom, |z| z)
.accept_when(|p| dropped_dock_widget(p).is_some())
.on_region_drop(move |region, payload, _pos, ctx| {
let Some(dock) = dropped_dock_widget(&payload) else {
return false;
};
match region {
DropRegion::Center => split_model.stack_into_tab(dock, side, tab_idx),
DropRegion::Leading | DropRegion::Top => {
split_model.split_into_tab(dock, side, tab_idx, pane_idx, true)
}
DropRegion::Trailing | DropRegion::Bottom => {
split_model.split_into_tab(dock, side, tab_idx, pane_idx, false)
}
}
ctx.request_accessibility_update();
true
});
let root = ctx.add(target);
self.root = Some(root);
let tab_model = self.model.clone();
ctx.apply_self_handlers(
HandlerSet::new()
.on_drag_hover(move |payload, _pos, _ctx| {
if dropped_dock_tab(payload).is_some() {
DropFeedback::Accept
} else {
DropFeedback::NoFeedback
}
})
.on_drop(move |payload, _pos, ctx| {
if let Some(tab_id) = dropped_dock_tab(&payload) {
let at = tab_model.side_append_index(side);
tab_model.move_tab(tab_id, side, at);
ctx.request_accessibility_update();
true
} else {
false
}
}),
);
vec![root]
}
fn layout_response(&self, proposal: SizeProposal, ctx: &LayoutContext) -> LayoutResponse {
self.root
.and_then(|id| ctx.child_layout_response(id, proposal))
.unwrap_or_else(|| proposal.resolve(0.0, 0.0).into())
}
fn place_children(
&self,
bounds: Rect,
_proposal: SizeProposal,
children: &mut [WidgetPlacement],
_ctx: &LayoutContext,
) {
for child in children.iter_mut() {
child.origin = bounds.origin();
child.size = bounds.size();
}
}
fn clips_children(&self) -> bool {
true
}
fn children(&self) -> Vec<WidgetId> {
self.root.into_iter().collect()
}
}