#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum DockCorner {
BottomLeft,
BottomRight,
TopLeft,
TopRight,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum Layout {
Overlay,
Inline,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum Opacity {
Solid,
Translucent,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum DockContent {
Text(String),
LogTail {
path: std::path::PathBuf,
max_lines: usize,
},
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct DockWidget {
pub id: usize,
pub corner: DockCorner,
pub width_frac: f32,
pub height_frac: f32,
pub title: String,
pub content: DockContent,
#[serde(default = "default_layout")]
pub layout: Layout,
#[serde(default = "default_opacity")]
pub opacity: Opacity,
}
fn default_layout() -> Layout {
Layout::Overlay
}
fn default_opacity() -> Opacity {
Opacity::Solid
}
impl DockWidget {
pub fn new_text<S: Into<String>>(id: usize, title: S, body: S) -> Self {
Self::new_text_at(id, DockCorner::BottomLeft, title, body)
}
pub fn new_text_at<S: Into<String>>(id: usize, corner: DockCorner, title: S, body: S) -> Self {
DockWidget {
id,
corner,
width_frac: 0.5,
height_frac: 0.25,
title: title.into(),
content: DockContent::Text(body.into()),
layout: Layout::Overlay,
opacity: Opacity::Solid,
}
}
}
pub fn push_text_at(app: &mut crate::app::App, corner: DockCorner) {
let id = app.dock_widget_next_id;
app.dock_widget_next_id += 1;
let n = app.dock_widgets.len() + 1;
app.dock_widgets.push(DockWidget::new_text_at(
id,
corner,
format!("Note {n}"),
format!(
"Dock widget #{n} at {corner:?}.\nUse the ⋮ menu → Close, or run `dock.close_all` to clear them all."
),
));
}
pub fn push_log_tail(app: &mut crate::app::App, corner: DockCorner, path: std::path::PathBuf) {
let id = app.dock_widget_next_id;
app.dock_widget_next_id += 1;
let title = path
.file_name()
.and_then(|n| n.to_str())
.map(|s| s.to_string())
.unwrap_or_else(|| "log".to_string());
app.dock_widgets.push(DockWidget {
id,
corner,
width_frac: 0.5,
height_frac: 0.25,
title,
content: DockContent::LogTail {
path,
max_lines: 16,
},
layout: Layout::Overlay,
opacity: Opacity::Solid,
});
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SizePreset {
Small,
Medium,
Large,
Wide,
Tall,
}
impl SizePreset {
pub fn fractions(self) -> (f32, f32) {
match self {
SizePreset::Small => (0.25, 0.15),
SizePreset::Medium => (0.5, 0.25),
SizePreset::Large => (0.5, 0.4),
SizePreset::Wide => (0.9, 0.25),
SizePreset::Tall => (0.5, 0.5),
}
}
pub fn label(self) -> &'static str {
match self {
SizePreset::Small => "Small",
SizePreset::Medium => "Medium",
SizePreset::Large => "Large",
SizePreset::Wide => "Wide",
SizePreset::Tall => "Tall",
}
}
}
#[derive(Debug, Clone, Copy)]
pub enum KebabMenuItem {
Header(&'static str),
Separator,
Resize(SizePreset),
MoveTo(DockCorner),
SetLayout(Layout),
SetOpacity(Opacity),
Rename,
Close,
}
#[derive(Debug, Clone)]
pub struct KebabMenuState {
pub widget_id: usize,
pub anchor_x: u16,
pub anchor_y: u16,
pub selected: usize,
pub items: Vec<KebabMenuItem>,
}
impl KebabMenuState {
pub fn build(widget: &DockWidget, anchor_x: u16, anchor_y: u16) -> Self {
let mut items = Vec::new();
items.push(KebabMenuItem::Header("Resize"));
for p in [
SizePreset::Small,
SizePreset::Medium,
SizePreset::Large,
SizePreset::Wide,
SizePreset::Tall,
] {
items.push(KebabMenuItem::Resize(p));
}
items.push(KebabMenuItem::Separator);
items.push(KebabMenuItem::Header("Move to"));
for c in [
DockCorner::BottomLeft,
DockCorner::BottomRight,
DockCorner::TopLeft,
DockCorner::TopRight,
] {
items.push(KebabMenuItem::MoveTo(c));
}
items.push(KebabMenuItem::Separator);
items.push(KebabMenuItem::Header("Layout"));
items.push(KebabMenuItem::SetLayout(Layout::Overlay));
items.push(KebabMenuItem::SetLayout(Layout::Inline));
items.push(KebabMenuItem::Separator);
items.push(KebabMenuItem::Header("Opacity"));
items.push(KebabMenuItem::SetOpacity(Opacity::Solid));
items.push(KebabMenuItem::SetOpacity(Opacity::Translucent));
items.push(KebabMenuItem::Separator);
items.push(KebabMenuItem::Rename);
items.push(KebabMenuItem::Close);
let current_preset = match_current_preset(widget);
let selected = items
.iter()
.position(|it| match it {
KebabMenuItem::Resize(p) => Some(*p) == current_preset,
_ => false,
})
.unwrap_or(1);
KebabMenuState {
widget_id: widget.id,
anchor_x,
anchor_y,
selected,
items,
}
}
}
fn match_current_preset(widget: &DockWidget) -> Option<SizePreset> {
let (wf, hf) = (widget.width_frac, widget.height_frac);
for p in [
SizePreset::Small,
SizePreset::Medium,
SizePreset::Large,
SizePreset::Wide,
SizePreset::Tall,
] {
let (pw, ph) = p.fractions();
if (wf - pw).abs() < 0.01 && (hf - ph).abs() < 0.01 {
return Some(p);
}
}
None
}
pub fn apply_kebab_choice(app: &mut crate::app::App, widget_id: usize, item: KebabMenuItem) {
match item {
KebabMenuItem::Header(_) | KebabMenuItem::Separator => {}
KebabMenuItem::Resize(preset) => {
if let Some(w) = app.dock_widgets.iter_mut().find(|w| w.id == widget_id) {
let (wf, hf) = preset.fractions();
w.width_frac = wf;
w.height_frac = hf;
}
}
KebabMenuItem::MoveTo(corner) => {
if let Some(w) = app.dock_widgets.iter_mut().find(|w| w.id == widget_id) {
w.corner = corner;
}
}
KebabMenuItem::SetLayout(layout) => {
if let Some(w) = app.dock_widgets.iter_mut().find(|w| w.id == widget_id) {
w.layout = layout;
}
}
KebabMenuItem::SetOpacity(opacity) => {
if let Some(w) = app.dock_widgets.iter_mut().find(|w| w.id == widget_id) {
w.opacity = opacity;
}
}
KebabMenuItem::Rename => {
if let Some(w) = app.dock_widgets.iter().find(|w| w.id == widget_id) {
let seed = w.title.clone();
app.dock_rename_target = Some(widget_id);
app.open_dock_rename_prompt(seed);
}
}
KebabMenuItem::Close => {
app.dock_widgets.retain(|w| w.id != widget_id);
}
}
app.dock_kebab_menu = None;
}
pub fn cycle_focused_corner(app: &mut crate::app::App) {
let Some(last) = app.dock_widgets.last_mut() else {
return;
};
last.corner = match last.corner {
DockCorner::BottomLeft => DockCorner::BottomRight,
DockCorner::BottomRight => DockCorner::TopRight,
DockCorner::TopRight => DockCorner::TopLeft,
DockCorner::TopLeft => DockCorner::BottomLeft,
};
}