use std::hash::Hash;
use freya_core::prelude::*;
use torin::{
content::Content,
direction::Direction,
position::Position,
size::Size,
};
use crate::{
drag_drop::{
DragZone,
DropZone,
use_drag,
},
resizable_container::{
PanelSize,
ResizableContainer,
ResizablePanel,
},
};
#[derive(Clone, PartialEq, Debug)]
pub struct DockPanel<TabId, PanelId> {
pub panel_id: PanelId,
pub tabs: Vec<TabId>,
pub active_tab_id: Option<TabId>,
}
impl<TabId, PanelId> DockPanel<TabId, PanelId> {
pub fn new(panel_id: PanelId, tabs: Vec<TabId>) -> Self
where
TabId: Clone,
{
let active_tab_id = tabs.first().cloned();
Self {
panel_id,
tabs,
active_tab_id,
}
}
pub fn insert_tab(&mut self, tab_id: TabId, position: usize)
where
TabId: Clone + PartialEq,
{
let target = match self.tabs.iter().position(|item| *item == tab_id) {
Some(existing) => {
self.tabs.remove(existing);
if position > existing {
position - 1
} else {
position
}
}
None => position,
};
self.active_tab_id = Some(tab_id.clone());
self.tabs.insert(target.min(self.tabs.len()), tab_id);
}
pub fn append_tab(&mut self, tab_id: TabId)
where
TabId: Clone + PartialEq,
{
self.tabs.retain(|item| *item != tab_id);
self.active_tab_id = Some(tab_id.clone());
self.tabs.push(tab_id);
}
}
#[derive(Clone, PartialEq, Debug)]
pub enum DockNode<TabId, PanelId> {
Split {
direction: Direction,
children: Vec<DockNode<TabId, PanelId>>,
},
Panel(DockPanel<TabId, PanelId>),
}
impl<TabId, PanelId> DockNode<TabId, PanelId>
where
TabId: Clone + PartialEq,
PanelId: Copy + PartialEq,
{
pub fn is_empty(&self) -> bool {
match self {
DockNode::Panel(panel) => panel.tabs.is_empty(),
DockNode::Split { children, .. } => children.is_empty(),
}
}
pub fn panel(&self, panel_id: &PanelId) -> Option<&DockPanel<TabId, PanelId>> {
match self {
DockNode::Panel(panel) => (&panel.panel_id == panel_id).then_some(panel),
DockNode::Split { children, .. } => {
children.iter().find_map(|child| child.panel(panel_id))
}
}
}
pub fn panel_mut(&mut self, panel_id: &PanelId) -> Option<&mut DockPanel<TabId, PanelId>> {
match self {
DockNode::Panel(panel) => (&panel.panel_id == panel_id).then_some(panel),
DockNode::Split { children, .. } => children
.iter_mut()
.find_map(|child| child.panel_mut(panel_id)),
}
}
pub fn find_tab(&self, tab_id: &TabId) -> Option<(PanelId, usize)> {
match self {
DockNode::Panel(panel) => panel
.tabs
.iter()
.position(|item| item == tab_id)
.map(|position| (panel.panel_id, position)),
DockNode::Split { children, .. } => {
children.iter().find_map(|child| child.find_tab(tab_id))
}
}
}
pub fn remove_tab_except(&mut self, tab_id: &TabId, except_panel_id: Option<&PanelId>) -> bool {
match self {
DockNode::Panel(panel) => {
if except_panel_id == Some(&panel.panel_id) {
return false;
}
let Some(position) = panel.tabs.iter().position(|item| item == tab_id) else {
return false;
};
panel.tabs.remove(position);
if panel.active_tab_id.as_ref() == Some(tab_id) {
panel.active_tab_id = panel.tabs.first().cloned();
}
true
}
DockNode::Split { children, .. } => children
.iter_mut()
.any(|child| child.remove_tab_except(tab_id, except_panel_id)),
}
}
pub fn split_panel(
&mut self,
panel_id: &PanelId,
side: Side,
new_panel: &DockPanel<TabId, PanelId>,
) -> bool {
match self {
DockNode::Panel(panel) if &panel.panel_id == panel_id => {
let new_dock_node = DockNode::Split {
direction: side.direction(),
children: Vec::new(),
};
let old_dock_node = std::mem::replace(self, new_dock_node);
if let DockNode::Split { children, .. } = self {
let new_node = DockNode::Panel(new_panel.clone());
*children = match side {
Side::Left | Side::Top => vec![new_node, old_dock_node],
Side::Right | Side::Bottom => vec![old_dock_node, new_node],
};
}
true
}
DockNode::Split { children, .. } => children
.iter_mut()
.any(|child| child.split_panel(panel_id, side, new_panel)),
_ => false,
}
}
pub fn close_empty_panels(&mut self) {
let DockNode::Split { children, .. } = self else {
return;
};
children.iter_mut().for_each(DockNode::close_empty_panels);
children.retain(|child| !child.is_empty());
if children.len() == 1 {
*self = children.remove(0);
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Side {
Top,
Bottom,
Left,
Right,
}
impl Side {
pub fn direction(self) -> Direction {
match self {
Side::Left | Side::Right => Direction::Horizontal,
Side::Top | Side::Bottom => Direction::Vertical,
}
}
}
#[derive(Clone, PartialEq, Debug)]
pub enum DropTarget<PanelId> {
Tab { panel_id: PanelId, position: usize },
Center(PanelId),
Split { panel_id: PanelId, side: Side },
}
pub trait DockingModel: 'static {
type TabId: Copy + PartialEq + Hash + 'static;
type PanelId: Copy + PartialEq + 'static;
type DropValue: Clone + PartialEq + 'static + From<Self::TabId>;
fn root(&self) -> Option<&DockNode<Self::TabId, Self::PanelId>>;
fn on_drop(&mut self, value: Self::DropValue, target: DropTarget<Self::PanelId>) -> bool;
fn set_active(&mut self, panel_id: Self::PanelId, tab_id: Self::TabId) -> bool;
}
#[derive(Clone, PartialEq, Debug)]
pub struct DockDrag<Value> {
value: Value,
}
impl<Value> DockDrag<Value> {
pub fn new(value: Value) -> Self {
Self { value }
}
}
#[derive(Clone, PartialEq, Debug)]
pub struct ContentContext<TabId, PanelId> {
pub panel_id: PanelId,
pub tab_id: Option<TabId>,
pub tab_count: usize,
}
pub struct TabBarContext<PanelId> {
pub panel_id: PanelId,
pub tab_children: Vec<Element>,
pub tab_count: usize,
}
#[derive(Clone, PartialEq, Debug)]
pub struct TabContext<TabId> {
pub tab_id: TabId,
pub is_drop_target: bool,
}
#[derive(Clone, Copy, PartialEq)]
enum HoverTarget<TabId> {
Tab(TabId),
Edge(Side),
Center,
}
fn toggle_hover<TabId: Copy + PartialEq + 'static>(
mut hover: State<Option<HoverTarget<TabId>>>,
target: HoverTarget<TabId>,
hovering: bool,
) {
if hovering {
hover.set(Some(target));
} else if hover() == Some(target) {
hover.set(None);
}
}
const MIDDLE_FLEX: f32 = 2.0;
#[derive(Clone, PartialEq)]
struct Renderers<TabId: 'static, PanelId: 'static> {
content: Callback<ContentContext<TabId, PanelId>, Element>,
tab: Callback<TabContext<TabId>, Element>,
drag: Callback<TabId, Element>,
bar: Callback<TabBarContext<PanelId>, Element>,
}
pub struct DockingArea<M: DockingModel> {
controller: Writable<M>,
renderers: Renderers<M::TabId, M::PanelId>,
preview_element: Option<Element>,
key: DiffKey,
}
impl<M: DockingModel> Clone for DockingArea<M> {
fn clone(&self) -> Self {
Self {
controller: self.controller.clone(),
renderers: self.renderers.clone(),
preview_element: self.preview_element.clone(),
key: self.key.clone(),
}
}
}
impl<M: DockingModel> PartialEq for DockingArea<M> {
fn eq(&self, other: &Self) -> bool {
self.controller == other.controller
&& self.renderers == other.renderers
&& self.preview_element == other.preview_element
&& self.key == other.key
}
}
impl<M: DockingModel> KeyExt for DockingArea<M> {
fn write_key(&mut self) -> &mut DiffKey {
&mut self.key
}
}
impl<M: DockingModel> DockingArea<M> {
pub fn new(
controller: impl Into<Writable<M>>,
render_content: impl Into<Callback<ContentContext<M::TabId, M::PanelId>, Element>>,
render_tab: impl Into<Callback<TabContext<M::TabId>, Element>>,
render_drag: impl Into<Callback<M::TabId, Element>>,
render_tab_bar: impl Into<Callback<TabBarContext<M::PanelId>, Element>>,
) -> Self {
Self {
controller: controller.into(),
renderers: Renderers {
content: render_content.into(),
tab: render_tab.into(),
drag: render_drag.into(),
bar: render_tab_bar.into(),
},
preview_element: None,
key: DiffKey::default(),
}
}
pub fn preview_element(mut self, element: impl IntoElement) -> Self {
self.preview_element = Some(element.into_element());
self
}
}
impl<M: DockingModel> Component for DockingArea<M> {
fn render(&self) -> impl IntoElement {
let controller = self.controller.clone();
let renderers = self.renderers.clone();
let preview_element = self.preview_element.clone();
let node = controller.read().root().cloned();
rect().expanded().map(node, move |element, root| {
element.child(render_node(root, controller, renderers, preview_element))
})
}
fn render_key(&self) -> DiffKey {
self.key.clone().or(self.default_key())
}
}
fn render_node<M: DockingModel>(
node: DockNode<M::TabId, M::PanelId>,
controller: Writable<M>,
renderers: Renderers<M::TabId, M::PanelId>,
preview_element: Option<Element>,
) -> Element {
match node {
DockNode::Split {
direction,
children,
} => {
let share = 100. / children.len().max(1) as f32;
ResizableContainer::new()
.direction(direction)
.panels_iter(children.into_iter().map(|child| {
ResizablePanel::new(PanelSize::percent(share))
.min_size(5.)
.child(render_node(
child,
controller.clone(),
renderers.clone(),
preview_element.clone(),
))
}))
.into_element()
}
DockNode::Panel(panel) => DockPanelView {
panel,
controller,
renderers,
preview_element,
key: DiffKey::default(),
}
.into_element(),
}
}
struct DockPanelView<M: DockingModel> {
panel: DockPanel<M::TabId, M::PanelId>,
controller: Writable<M>,
renderers: Renderers<M::TabId, M::PanelId>,
preview_element: Option<Element>,
key: DiffKey,
}
impl<M: DockingModel> Clone for DockPanelView<M> {
fn clone(&self) -> Self {
Self {
panel: self.panel.clone(),
controller: self.controller.clone(),
renderers: self.renderers.clone(),
preview_element: self.preview_element.clone(),
key: self.key.clone(),
}
}
}
impl<M: DockingModel> PartialEq for DockPanelView<M> {
fn eq(&self, _other: &Self) -> bool {
false
}
}
impl<M: DockingModel> KeyExt for DockPanelView<M> {
fn write_key(&mut self) -> &mut DiffKey {
&mut self.key
}
}
impl<M: DockingModel> ComponentOwned for DockPanelView<M> {
fn render(self) -> impl IntoElement {
let DockPanelView {
panel:
DockPanel {
panel_id,
tabs,
active_tab_id,
},
controller,
renderers,
preview_element,
..
} = self;
let drag = use_drag::<DockDrag<M::DropValue>>();
let is_dragging = drag.read().is_some();
let hover = use_state(|| None::<HoverTarget<M::TabId>>);
let hovered = is_dragging.then(&*hover).flatten();
let tab_count = tabs.len();
let mut tab_children: Vec<Element> = tabs
.iter()
.enumerate()
.map(|(index, &tab_id)| {
let handle = renderers.tab.call(TabContext {
tab_id,
is_drop_target: hovered == Some(HoverTarget::Tab(tab_id)),
});
let dragger = DragZone::<DockDrag<M::DropValue>>::new(
DockDrag::new(M::DropValue::from(tab_id)),
handle,
)
.drag_element(renderers.drag.call(tab_id))
.into_element();
let activatable = rect()
.on_press({
let mut controller = controller.clone();
move |_| {
controller.write().set_active(panel_id, tab_id);
}
})
.child(dragger)
.into_element();
DropZone::<DockDrag<M::DropValue>>::new(activatable, {
let mut controller = controller.clone();
move |payload: DockDrag<M::DropValue>| {
controller.write().on_drop(
payload.value,
DropTarget::Tab {
panel_id,
position: index,
},
);
}
})
.on_drag_over(move |hovering| {
toggle_hover(hover, HoverTarget::Tab(tab_id), hovering)
})
.key(tab_id)
.into_element()
})
.collect();
tab_children.push(
DropZone::<DockDrag<M::DropValue>>::new(rect().expanded().into_element(), {
let mut controller = controller.clone();
move |payload: DockDrag<M::DropValue>| {
controller.write().on_drop(
payload.value,
DropTarget::Tab {
panel_id,
position: tab_count,
},
);
}
})
.into_element(),
);
let tab_bar = renderers.bar.call(TabBarContext {
panel_id,
tab_children,
tab_count,
});
let content = renderers.content.call(ContentContext {
panel_id,
tab_id: active_tab_id,
tab_count,
});
let overlay = is_dragging.then(move || {
let ghost = match (hover(), preview_element) {
(Some(HoverTarget::Edge(side)), Some(preview)) => {
let (width, height) = match side {
Side::Top | Side::Bottom => (Size::percent(100.), Size::percent(50.)),
Side::Left | Side::Right => (Size::percent(50.), Size::percent(100.)),
};
let position = match side {
Side::Top | Side::Left => Position::new_absolute(),
Side::Bottom => Position::new_absolute().bottom(0.),
Side::Right => Position::new_absolute().right(0.),
};
Some(drag_preview(position, width, height, preview))
}
(Some(HoverTarget::Center), Some(preview)) => Some(drag_preview(
Position::new_absolute(),
Size::percent(100.),
Size::percent(100.),
preview,
)),
_ => None,
};
let edge = |side: Side, width: Size, height: Size| -> Element {
rect()
.width(width)
.height(height)
.child(drop_zone_for_side::<M>(
panel_id,
side,
controller.clone(),
hover,
))
.into_element()
};
let center_drop =
DropZone::<DockDrag<M::DropValue>>::new(rect().expanded().into_element(), {
let mut controller = controller.clone();
move |payload: DockDrag<M::DropValue>| {
controller
.write()
.on_drop(payload.value, DropTarget::Center(panel_id));
}
})
.on_drag_over(move |hovering| toggle_hover(hover, HoverTarget::Center, hovering))
.into_element();
let middle_row = rect()
.width(Size::percent(100.))
.height(Size::flex(MIDDLE_FLEX))
.horizontal()
.content(Content::flex())
.child(edge(Side::Left, Size::flex(1.), Size::percent(100.)))
.child(
rect()
.width(Size::flex(MIDDLE_FLEX))
.height(Size::percent(100.))
.child(center_drop),
)
.child(edge(Side::Right, Size::flex(1.), Size::percent(100.)));
rect()
.position(Position::new_absolute())
.layer(Layer::Overlay)
.width(Size::percent(100.))
.height(Size::percent(100.))
.vertical()
.content(Content::flex())
.maybe_child(ghost)
.child(edge(Side::Top, Size::percent(100.), Size::flex(1.)))
.child(middle_row)
.child(edge(Side::Bottom, Size::percent(100.), Size::flex(1.)))
.into_element()
});
rect()
.a11y_role(AccessibilityRole::Pane)
.expanded()
.child(tab_bar)
.child(
rect()
.expanded()
.overflow(Overflow::Clip)
.child(content)
.maybe_child(overlay),
)
}
fn render_key(&self) -> DiffKey {
self.key.clone().or(self.default_key())
}
}
fn drop_zone_for_side<M: DockingModel>(
panel_id: M::PanelId,
side: Side,
mut controller: Writable<M>,
hover: State<Option<HoverTarget<M::TabId>>>,
) -> Element {
DropZone::<DockDrag<M::DropValue>>::new(
rect().expanded().into_element(),
move |payload: DockDrag<M::DropValue>| {
controller
.write()
.on_drop(payload.value, DropTarget::Split { panel_id, side });
},
)
.on_drag_over(move |hovering| toggle_hover(hover, HoverTarget::Edge(side), hovering))
.into_element()
}
fn drag_preview(position: Position, width: Size, height: Size, preview: Element) -> Element {
rect()
.position(position)
.interactive(false)
.width(width)
.height(height)
.child(preview)
.into_element()
}