#![allow(dead_code)]
use crate::widget::layout::splitter::Pane;
use crate::widget::layout::tabs::Tabs;
use crate::widget::theme::PLACEHOLDER_FG;
use crate::widget::traits::{RenderContext, View, WidgetProps};
use crate::{impl_props_builders, impl_styled_view};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DockPosition {
Left,
Right,
Top,
Bottom,
Center,
}
pub struct DockArea {
id: String,
tabs: Vec<TabContent>,
active_tab: usize,
min_size: u16,
max_size: u16,
ratio: f32,
collapsible: bool,
collapsed: bool,
position: DockPosition,
min_width: u16,
min_height: u16,
max_width: u16,
max_height: u16,
props: WidgetProps,
}
pub struct TabContent {
label: String,
widget: Option<Box<dyn View>>,
}
impl TabContent {
pub fn new(label: impl Into<String>) -> Self {
Self {
label: label.into(),
widget: None,
}
}
pub fn widget<W: View + 'static>(mut self, widget: W) -> Self {
self.widget = Some(Box::new(widget));
self
}
}
impl DockArea {
pub fn new(id: impl Into<String>) -> Self {
Self {
id: id.into(),
tabs: Vec::new(),
active_tab: 0,
min_size: 100,
max_size: 0,
ratio: 0.2,
collapsible: false,
collapsed: false,
position: DockPosition::Left,
min_width: 0,
min_height: 0,
max_width: 0,
max_height: 0,
props: WidgetProps::new(),
}
}
pub fn position(mut self, position: DockPosition) -> Self {
self.position = position;
self
}
pub fn min_size(mut self, size: u16) -> Self {
self.min_size = size;
self
}
pub fn max_size(mut self, size: u16) -> Self {
self.max_size = size;
self
}
pub fn ratio(mut self, ratio: f32) -> Self {
self.ratio = ratio.clamp(0.0, 1.0);
self
}
pub fn collapsible(mut self) -> Self {
self.collapsible = true;
self
}
pub fn collapsed(mut self, collapsed: bool) -> Self {
self.collapsed = collapsed;
self
}
pub fn tab(mut self, label: impl Into<String>) -> Self {
self.tabs.push(TabContent::new(label.into()));
self
}
pub fn tab_with<W: View + 'static>(mut self, label: impl Into<String>, widget: W) -> Self {
self.tabs.push(TabContent::new(label.into()).widget(widget));
self
}
pub fn panel<W: View + 'static>(mut self, widget: W) -> Self {
let label = self.id.clone();
self.tabs.push(TabContent::new(label).widget(widget));
self
}
pub fn min_width(mut self, width: u16) -> Self {
self.min_width = width;
self
}
pub fn min_height(mut self, height: u16) -> Self {
self.min_height = height;
self
}
pub fn max_width(mut self, width: u16) -> Self {
self.max_width = width;
self
}
pub fn max_height(mut self, height: u16) -> Self {
self.max_height = height;
self
}
pub fn min_dimensions(self, width: u16, height: u16) -> Self {
self.min_width(width).min_height(height)
}
pub fn max_dimensions(self, width: u16, height: u16) -> Self {
self.max_width(width).max_height(height)
}
pub fn constrain(self, min_w: u16, min_h: u16, max_w: u16, max_h: u16) -> Self {
self.min_width(min_w)
.min_height(min_h)
.max_width(max_w)
.max_height(max_h)
}
fn to_pane(&self) -> Pane {
let mut pane = Pane::new(&self.id)
.min_size(self.min_size)
.max_size(self.max_size)
.ratio(self.ratio);
if self.collapsible {
pane = pane.collapsible();
}
pane.collapsed = self.collapsed;
pane
}
}
impl Clone for DockArea {
fn clone(&self) -> Self {
Self {
id: self.id.clone(),
tabs: Vec::new(), active_tab: self.active_tab,
min_size: self.min_size,
max_size: self.max_size,
ratio: self.ratio,
collapsible: self.collapsible,
collapsed: self.collapsed,
position: self.position,
min_width: self.min_width,
min_height: self.min_height,
max_width: self.max_width,
max_height: self.max_height,
props: self.props.clone(),
}
}
}
impl View for DockArea {
fn render(&self, ctx: &mut RenderContext) {
if self.collapsed {
return;
}
let rect = ctx.area;
if self.tabs.len() > 1 {
let tabs_labels: Vec<String> = self.tabs.iter().map(|t| t.label.clone()).collect();
let tabs = Tabs::new()
.tabs(tabs_labels)
.fg(PLACEHOLDER_FG)
.bg(crate::style::Color::rgb(0, 0, 0));
if rect.height > 1 {
let tab_rect = crate::layout::Rect::new(rect.x, rect.y, rect.width, 1);
let mut tab_ctx = RenderContext::new(ctx.buffer, tab_rect);
tabs.render(&mut tab_ctx);
if let Some(active_tab) = self.tabs.get(self.active_tab) {
if let Some(widget) = &active_tab.widget {
let content_rect = crate::layout::Rect::new(
rect.x,
rect.y + 1,
rect.width,
rect.height.saturating_sub(1),
);
let mut content_ctx = RenderContext::new(ctx.buffer, content_rect);
widget.render(&mut content_ctx);
}
}
}
} else if let Some(tab) = self.tabs.first() {
if let Some(widget) = &tab.widget {
widget.render(ctx);
}
}
}
}
impl_props_builders!(DockArea);
impl_styled_view!(DockArea);
pub struct DockManager {
left: Option<DockArea>,
right: Option<DockArea>,
top: Option<DockArea>,
bottom: Option<DockArea>,
center: Option<DockArea>,
min_width: u16,
min_height: u16,
max_width: u16,
max_height: u16,
props: WidgetProps,
}
impl DockManager {
pub fn new() -> Self {
Self {
left: None,
right: None,
top: None,
bottom: None,
center: None,
min_width: 0,
min_height: 0,
max_width: 0,
max_height: 0,
props: WidgetProps::new(),
}
}
pub fn left(mut self, area: DockArea) -> Self {
self.left = Some(area.position(DockPosition::Left));
self
}
pub fn right(mut self, area: DockArea) -> Self {
self.right = Some(area.position(DockPosition::Right));
self
}
pub fn top(mut self, area: DockArea) -> Self {
self.top = Some(area.position(DockPosition::Top));
self
}
pub fn bottom(mut self, area: DockArea) -> Self {
self.bottom = Some(area.position(DockPosition::Bottom));
self
}
pub fn center(mut self, area: DockArea) -> Self {
self.center = Some(area.position(DockPosition::Center));
self
}
pub fn min_width(mut self, width: u16) -> Self {
self.min_width = width;
self
}
pub fn min_height(mut self, height: u16) -> Self {
self.min_height = height;
self
}
pub fn max_width(mut self, width: u16) -> Self {
self.max_width = width;
self
}
pub fn max_height(mut self, height: u16) -> Self {
self.max_height = height;
self
}
pub fn min_size(self, width: u16, height: u16) -> Self {
self.min_width(width).min_height(height)
}
pub fn max_size(self, width: u16, height: u16) -> Self {
self.max_width(width).max_height(height)
}
pub fn constrain(self, min_w: u16, min_h: u16, max_w: u16, max_h: u16) -> Self {
self.min_width(min_w)
.min_height(min_h)
.max_width(max_w)
.max_height(max_h)
}
fn calculate_layout(&self, rect: crate::layout::Rect) -> Vec<(DockArea, crate::layout::Rect)> {
let mut layout = Vec::new();
let mut current = rect;
if let Some(ref top) = self.top {
if !top.collapsed {
let top_height =
(current.height as f32 * top.ratio).max(top.min_size as f32) as u16;
let top_rect = crate::layout::Rect::new(
current.x,
current.y,
current.width,
top_height.min(current.height),
);
layout.push(((*top).clone(), top_rect));
current.y += top_height;
current.height = current.height.saturating_sub(top_height);
}
}
if let Some(ref bottom) = self.bottom {
if !bottom.collapsed {
let bottom_height =
(current.height as f32 * bottom.ratio).max(bottom.min_size as f32) as u16;
let bottom_rect = crate::layout::Rect::new(
current.x,
current.y
+ current
.height
.saturating_sub(bottom_height.min(current.height)),
current.width,
bottom_height.min(current.height),
);
layout.push(((*bottom).clone(), bottom_rect));
current.height = current
.height
.saturating_sub(bottom_height.min(current.height));
}
}
let mut middle = current;
if let Some(ref left) = self.left {
if !left.collapsed {
let left_width =
(middle.width as f32 * left.ratio).max(left.min_size as f32) as u16;
let left_rect = crate::layout::Rect::new(
middle.x,
middle.y,
left_width.min(middle.width),
middle.height,
);
layout.push(((*left).clone(), left_rect));
middle.x += left_width;
middle.width = middle.width.saturating_sub(left_width);
}
}
if let Some(ref right) = self.right {
if !right.collapsed {
let right_width =
(middle.width as f32 * right.ratio).max(right.min_size as f32) as u16;
let right_rect = crate::layout::Rect::new(
middle.x + middle.width.saturating_sub(right_width.min(middle.width)),
middle.y,
right_width.min(middle.width),
middle.height,
);
layout.push(((*right).clone(), right_rect));
middle.width = middle.width.saturating_sub(right_width);
}
}
if let Some(ref center) = self.center {
layout.push(((*center).clone(), middle));
}
layout
}
}
impl Default for DockManager {
fn default() -> Self {
Self::new()
}
}
impl View for DockManager {
fn render(&self, ctx: &mut RenderContext) {
let rect = ctx.area;
let layout = self.calculate_layout(rect);
for (area, area_rect) in layout {
let mut area_ctx = RenderContext::new(ctx.buffer, area_rect);
area.render(&mut area_ctx);
}
}
}
impl_props_builders!(DockManager);
impl_styled_view!(DockManager);
pub fn dock() -> DockManager {
DockManager::new()
}
pub fn dock_area(id: impl Into<String>) -> DockArea {
DockArea::new(id)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::widget::Text;
#[test]
fn test_dock_area_new() {
let area = DockArea::new("test");
assert_eq!(area.id, "test");
assert!(area.tabs.is_empty());
assert_eq!(area.position, DockPosition::Left);
assert!(!area.collapsible);
assert!(!area.collapsed);
}
#[test]
fn test_dock_area_builder() {
let area = DockArea::new("sidebar")
.position(DockPosition::Right)
.min_size(50)
.ratio(0.3)
.collapsible()
.tab("Files")
.tab("Search");
assert_eq!(area.position, DockPosition::Right);
assert_eq!(area.min_size, 50);
assert_eq!(area.ratio, 0.3);
assert!(area.collapsible);
assert_eq!(area.tabs.len(), 2);
}
#[test]
fn test_dock_area_tab_with_widget() {
let area = DockArea::new("editor").tab_with("main.rs", Text::new("code"));
assert_eq!(area.tabs.len(), 1);
assert!(area.tabs[0].widget.is_some());
}
#[test]
fn test_dock_position_variants() {
assert_eq!(DockPosition::Left, DockPosition::Left);
assert_ne!(DockPosition::Left, DockPosition::Right);
}
#[test]
fn test_dock_area_helper() {
let a = dock_area("test");
assert_eq!(a.id, "test");
}
}