use std::collections::{HashMap, HashSet};
use crate::descriptor_store::{DescriptorStore, Handle};
use crate::event::Rect;
use crate::primitives::activity_bar::ActivityBar;
use crate::primitives::board::BoardModel;
use crate::primitives::canvas::Canvas;
use crate::primitives::chart::Chart;
use crate::primitives::command_center::CommandCenter;
use crate::primitives::command_line::CommandLine;
use crate::primitives::completions::{Completions, CompletionsLayout};
use crate::primitives::context_menu::{ContextMenu, ContextMenuLayout};
use crate::primitives::data_table::DataTable;
use crate::primitives::dialog::{Dialog, DialogLayout};
use crate::primitives::diff_view::DiffView;
use crate::primitives::drop_zone::DropOverlay;
use crate::primitives::editor::Editor;
use crate::primitives::find_replace::FindReplacePanel;
use crate::primitives::float::{Float, FloatLayout};
use crate::primitives::form::Form;
use crate::primitives::image::Image;
use crate::primitives::list::ListView;
use crate::primitives::menu_bar::MenuBar;
use crate::primitives::message_list::MessageList;
use crate::primitives::minimap::Minimap;
use crate::primitives::multi_section_view::MultiSectionView;
use crate::primitives::palette::Palette;
use crate::primitives::panel::Panel;
use crate::primitives::pipeline_view::PipelineView;
use crate::primitives::progress::ProgressBar;
use crate::primitives::rich_text_popup::{RichTextPopup, RichTextPopupLayout};
use crate::primitives::scrollbar::Scrollbar;
use crate::primitives::sidebar_panel::SidebarPanel;
use crate::primitives::spinner::Spinner;
use crate::primitives::split::Split;
use crate::primitives::split_tree::SplitTree;
use crate::primitives::status_bar::StatusBar;
use crate::primitives::tab_bar::TabBar;
use crate::primitives::terminal::Terminal;
use crate::primitives::text_display::TextDisplay;
use crate::primitives::text_input::TextInput;
use crate::primitives::toast::ToastOverlay;
use crate::primitives::toolbar::Toolbar;
use crate::primitives::tooltip::{Tooltip, TooltipLayout};
use crate::primitives::tree::TreeView;
use crate::types::WidgetId;
use crate::Backend;
#[allow(clippy::large_enum_variant)]
#[non_exhaustive]
pub enum Surface {
Editor {
rect: Rect,
editor: Editor,
},
TabBar {
rect: Rect,
bar: TabBar,
hovered_close: Option<usize>,
},
StatusBar {
rect: Rect,
bar: StatusBar,
hovered: Option<WidgetId>,
pressed: Option<WidgetId>,
},
ActivityBar {
rect: Rect,
bar: ActivityBar,
hovered: Option<usize>,
},
CommandLine {
rect: Rect,
cmd: CommandLine,
},
Terminal {
rect: Rect,
term: Terminal,
},
TextDisplay {
rect: Rect,
td: TextDisplay,
},
MultiSectionView {
rect: Rect,
view: MultiSectionView,
},
Tree {
rect: Rect,
tree: TreeView,
},
List {
rect: Rect,
list: ListView,
},
Form {
rect: Rect,
form: Form,
},
MenuBar {
rect: Rect,
bar: MenuBar,
},
Split {
rect: Rect,
split: Split,
},
Panel {
rect: Rect,
panel: Panel,
},
Scrollbar {
rect: Rect,
sb: Scrollbar,
},
Palette {
rect: Rect,
palette: Palette,
},
Tooltip {
tooltip: Tooltip,
layout: TooltipLayout,
},
ContextMenu {
menu: ContextMenu,
layout: ContextMenuLayout,
},
Dialog {
dialog: Dialog,
layout: DialogLayout,
},
Float {
float: Float,
layout: FloatLayout,
},
Completions {
completions: Completions,
layout: CompletionsLayout,
},
FindReplace {
rect: Rect,
panel: FindReplacePanel,
},
RichTextPopup {
popup: RichTextPopup,
layout: RichTextPopupLayout,
},
Toast {
rect: Rect,
stack: ToastOverlay,
},
DataTable {
rect: Rect,
table: DataTable,
hovered: Option<usize>,
},
Chart {
rect: Rect,
chart: Chart,
hovered_point: Option<(usize, usize)>,
crosshair_x: Option<f64>,
},
Canvas {
rect: Rect,
canvas: Canvas,
},
Board {
rect: Rect,
model: BoardModel,
},
CommandCenter {
rect: Rect,
cc: CommandCenter,
},
DiffView {
rect: Rect,
view: DiffView,
},
DropOverlay {
overlay: DropOverlay,
},
Image {
rect: Rect,
image: Image,
},
MessageList {
rect: Rect,
list: MessageList,
},
Minimap {
rect: Rect,
minimap: Minimap,
},
PipelineView {
rect: Rect,
view: PipelineView,
},
Progress {
rect: Rect,
bar: ProgressBar,
},
SidebarPanel {
rect: Rect,
panel: SidebarPanel,
hovered: Option<WidgetId>,
pressed: Option<WidgetId>,
},
Spinner {
rect: Rect,
spinner: Spinner,
},
SplitTree {
rect: Rect,
tree: SplitTree,
},
TextInput {
rect: Rect,
ti: TextInput,
},
Toolbar {
rect: Rect,
bar: Toolbar,
hovered: Option<WidgetId>,
pressed: Option<WidgetId>,
},
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
pub enum FrameZone {
Editor { idx: usize },
TabBar { idx: usize },
StatusBar { idx: usize },
ActivityBar { idx: usize },
CommandLine { idx: usize },
Terminal { idx: usize },
TextDisplay { idx: usize },
MultiSectionView { idx: usize },
Tree { idx: usize },
List { idx: usize },
Form { idx: usize },
MenuBar { idx: usize },
Split { idx: usize },
Panel { idx: usize },
Scrollbar { idx: usize },
Palette { idx: usize },
Tooltip { idx: usize },
ContextMenu { idx: usize },
Dialog { idx: usize },
Float { idx: usize },
Completions { idx: usize },
FindReplace { idx: usize },
RichTextPopup { idx: usize },
Toast { idx: usize },
DataTable { idx: usize },
Chart { idx: usize },
Canvas { idx: usize },
Board { idx: usize },
CommandCenter { idx: usize },
DiffView { idx: usize },
DropOverlay { idx: usize },
Image { idx: usize },
MessageList { idx: usize },
Minimap { idx: usize },
PipelineView { idx: usize },
Progress { idx: usize },
SidebarPanel { idx: usize },
Spinner { idx: usize },
SplitTree { idx: usize },
TextInput { idx: usize },
Toolbar { idx: usize },
Empty,
}
#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct FrameHitMap {
zones: Vec<(Rect, FrameZone)>,
}
impl FrameHitMap {
fn new() -> Self {
Self::default()
}
fn push(&mut self, rect: Rect, zone: FrameZone) {
self.zones.push((rect, zone));
}
pub fn from_zones(zones: Vec<(Rect, FrameZone)>) -> Self {
Self { zones }
}
pub fn zones(&self) -> &[(Rect, FrameZone)] {
&self.zones
}
pub fn hit_test(&self, x: f32, y: f32) -> FrameZone {
for (rect, zone) in self.zones.iter().rev() {
if x >= rect.x && x < rect.x + rect.width && y >= rect.y && y < rect.y + rect.height {
return zone.clone();
}
}
FrameZone::Empty
}
pub fn len(&self) -> usize {
self.zones.len()
}
pub fn is_empty(&self) -> bool {
self.zones.is_empty()
}
}
pub struct ScreenLayout {
surfaces: Vec<Surface>,
tree_store: DescriptorStore<TreeView>,
tree_handles: HashMap<WidgetId, Handle<TreeView>>,
}
impl ScreenLayout {
pub fn new() -> Self {
Self {
surfaces: Vec::new(),
tree_store: DescriptorStore::new(),
tree_handles: HashMap::new(),
}
}
pub fn push(&mut self, surface: Surface) {
self.surfaces.push(surface);
}
pub fn surfaces(&self) -> &[Surface] {
&self.surfaces
}
pub fn surfaces_mut(&mut self) -> &mut [Surface] {
&mut self.surfaces
}
pub fn tree_frame(&mut self) -> Vec<(Handle<TreeView>, Rect)> {
let mut frame = Vec::new();
let mut seen = HashSet::new();
for surface in &self.surfaces {
if let Surface::Tree { rect, tree } = surface {
seen.insert(tree.id);
let handle = match self.tree_handles.get(&tree.id) {
Some(&handle) => {
if self.tree_store.get(handle) != Some(tree) {
self.tree_store.update(handle, |slot| *slot = tree.clone());
}
handle
}
None => {
let handle = self.tree_store.insert(tree.clone());
self.tree_handles.insert(tree.id, handle);
handle
}
};
frame.push((handle, *rect));
}
}
let stale: Vec<WidgetId> = self
.tree_handles
.keys()
.filter(|id| !seen.contains(*id))
.copied()
.collect();
for id in stale {
if let Some(handle) = self.tree_handles.remove(&id) {
self.tree_store.remove(handle);
}
}
frame
}
pub fn draw(&self, backend: &mut dyn Backend) -> FrameHitMap {
let mut hit_map = FrameHitMap::new();
for (idx, surface) in self.surfaces.iter().enumerate() {
match surface {
Surface::Editor { rect, editor } => {
backend.draw_editor(*rect, editor);
}
Surface::TabBar {
rect,
bar,
hovered_close,
} => {
backend.draw_tab_bar(*rect, bar, *hovered_close);
}
Surface::StatusBar {
rect,
bar,
hovered,
pressed,
} => {
let interaction =
crate::interaction::InteractionState::from_parts(*hovered, *pressed);
backend.draw_status_bar_interactive(*rect, bar, &interaction);
}
Surface::ActivityBar { rect, bar, hovered } => {
backend.draw_activity_bar(*rect, bar, *hovered);
}
Surface::CommandLine { rect, cmd } => {
backend.draw_command_line(*rect, cmd);
}
Surface::Terminal { rect, term } => {
backend.draw_terminal(*rect, term);
}
Surface::TextDisplay { rect, td } => {
backend.draw_text_display(*rect, td);
}
Surface::MultiSectionView { rect, view } => {
backend.draw_multi_section_view(*rect, view);
}
Surface::Tree { rect, tree } => {
backend.draw_tree(*rect, tree);
}
Surface::List { rect, list } => {
backend.draw_list(*rect, list);
}
Surface::Form { rect, form } => {
backend.draw_form(*rect, form);
}
Surface::MenuBar { rect, bar } => {
backend.draw_menu_bar(*rect, bar);
}
Surface::Split { rect, split } => {
backend.draw_split(*rect, split);
}
Surface::Panel { rect, panel } => {
backend.draw_panel(*rect, panel);
}
Surface::Scrollbar { rect, sb } => {
backend.draw_scrollbar(*rect, sb);
}
Surface::Palette { rect, palette } => {
backend.draw_palette(*rect, palette);
}
Surface::Tooltip { tooltip, layout } => {
backend.draw_tooltip(tooltip, layout);
}
Surface::ContextMenu { menu, layout } => {
backend.draw_context_menu(menu, layout);
}
Surface::Dialog { dialog, layout } => {
backend.draw_dialog(dialog, layout);
}
Surface::Float { float, layout } => {
backend.draw_float(float, layout);
}
Surface::Completions {
completions,
layout,
} => {
backend.draw_completions(completions, layout);
}
Surface::FindReplace { rect, panel } => {
backend.draw_find_replace(*rect, panel);
}
Surface::RichTextPopup { popup, layout } => {
backend.draw_rich_text_popup(popup, layout);
}
Surface::Toast { rect, stack } => {
backend.draw_toast_overlay(*rect, stack);
}
Surface::DataTable {
rect,
table,
hovered,
} => {
backend.draw_data_table(*rect, table, *hovered);
}
Surface::Chart {
rect,
chart,
hovered_point,
crosshair_x,
} => {
backend.draw_chart(*rect, chart, *hovered_point, *crosshair_x);
}
Surface::Canvas { rect, canvas } => {
backend.draw_canvas(*rect, canvas);
}
Surface::Board { rect, model } => {
backend.draw_board(*rect, model);
}
Surface::CommandCenter { rect, cc } => {
backend.draw_command_center(*rect, cc);
}
Surface::DiffView { rect, view } => {
backend.draw_diff_view(*rect, view);
}
Surface::DropOverlay { overlay } => {
backend.draw_drop_overlay(overlay);
}
Surface::Image { rect, image } => {
backend.draw_image(*rect, image);
}
Surface::MessageList { rect, list } => {
backend.draw_message_list(*rect, list);
}
Surface::Minimap { rect, minimap } => {
backend.draw_minimap(*rect, minimap);
}
Surface::PipelineView { rect, view } => {
backend.draw_pipeline_view(*rect, view);
}
Surface::Progress { rect, bar } => {
backend.draw_progress(*rect, bar);
}
Surface::SidebarPanel {
rect,
panel,
hovered,
pressed,
} => {
let interaction =
crate::interaction::InteractionState::from_parts(*hovered, *pressed);
backend.draw_sidebar_panel_interactive(*rect, panel, &interaction);
}
Surface::Spinner { rect, spinner } => {
backend.draw_spinner(*rect, spinner);
}
Surface::SplitTree { rect, tree } => {
backend.draw_split_tree(*rect, tree);
}
Surface::TextInput { rect, ti } => {
backend.draw_text_input(*rect, ti);
}
Surface::Toolbar {
rect,
bar,
hovered,
pressed,
} => {
let interaction =
crate::interaction::InteractionState::from_parts(*hovered, *pressed);
backend.draw_toolbar_interactive(*rect, bar, &interaction);
}
}
let (rect, zone) = Self::zone_for(idx, surface);
hit_map.push(rect, zone);
}
hit_map
}
pub fn hit_map(&self) -> FrameHitMap {
let mut hit_map = FrameHitMap::new();
for (idx, surface) in self.surfaces.iter().enumerate() {
let (rect, zone) = Self::zone_for(idx, surface);
hit_map.push(rect, zone);
}
hit_map
}
pub fn tab_stops(&self) -> Vec<(WidgetId, Rect)> {
let mut stops: Vec<(WidgetId, Rect)> = self
.surfaces
.iter()
.filter_map(|s| match s {
Surface::Editor { rect, editor } => Some((editor.id, *rect)),
Surface::TabBar { rect, bar, .. } => Some((bar.id, *rect)),
Surface::StatusBar { rect, bar, .. } => Some((bar.id, *rect)),
Surface::ActivityBar { rect, bar, .. } => Some((bar.id, *rect)),
Surface::CommandLine { rect, cmd } => Some((cmd.id, *rect)),
Surface::Terminal { rect, term } => Some((term.id, *rect)),
Surface::TextDisplay { rect, td } => Some((td.id, *rect)),
Surface::MultiSectionView { rect, view } => Some((view.id, *rect)),
Surface::Tree { rect, tree } => Some((tree.id, *rect)),
Surface::List { rect, list } => Some((list.id, *rect)),
Surface::Form { rect, form } => Some((form.id, *rect)),
Surface::MenuBar { rect, bar } => Some((bar.id, *rect)),
Surface::Panel { rect, panel } => Some((panel.id, *rect)),
Surface::Palette { rect, palette } => Some((palette.id, *rect)),
Surface::DataTable { rect, table, .. } => Some((table.id, *rect)),
Surface::Chart { rect, chart, .. } => Some((chart.id, *rect)),
Surface::Board { rect, model } => Some((model.id, *rect)),
Surface::CommandCenter { rect, cc } => Some((cc.id, *rect)),
Surface::DiffView { rect, view } => Some((view.id, *rect)),
Surface::MessageList { rect, list } => Some((list.id, *rect)),
Surface::Minimap { rect, minimap } => Some((minimap.id, *rect)),
Surface::PipelineView { rect, view } => Some((view.id, *rect)),
Surface::Progress { rect, bar } if bar.cancellable => Some((bar.id, *rect)),
Surface::SidebarPanel { rect, panel, .. } => Some((panel.id, *rect)),
Surface::TextInput { rect, ti } => Some((ti.id, *rect)),
Surface::Toolbar { rect, bar, .. } => Some((bar.id, *rect)),
Surface::Image { .. }
| Surface::Spinner { .. }
| Surface::Canvas { .. }
| Surface::Progress { .. }
| Surface::Split { .. }
| Surface::Scrollbar { .. }
| Surface::Tooltip { .. }
| Surface::ContextMenu { .. }
| Surface::Dialog { .. }
| Surface::Float { .. }
| Surface::Completions { .. }
| Surface::FindReplace { .. }
| Surface::RichTextPopup { .. }
| Surface::DropOverlay { .. }
| Surface::SplitTree { .. }
| Surface::Toast { .. } => None,
})
.collect();
stops.sort_by(|(_, a), (_, b)| {
a.y.partial_cmp(&b.y)
.unwrap_or(std::cmp::Ordering::Equal)
.then(a.x.partial_cmp(&b.x).unwrap_or(std::cmp::Ordering::Equal))
});
stops
}
fn zone_for(idx: usize, surface: &Surface) -> (Rect, FrameZone) {
match surface {
Surface::Editor { rect, .. } => (*rect, FrameZone::Editor { idx }),
Surface::TabBar { rect, .. } => (*rect, FrameZone::TabBar { idx }),
Surface::StatusBar { rect, .. } => (*rect, FrameZone::StatusBar { idx }),
Surface::ActivityBar { rect, .. } => (*rect, FrameZone::ActivityBar { idx }),
Surface::CommandLine { rect, .. } => (*rect, FrameZone::CommandLine { idx }),
Surface::Terminal { rect, .. } => (*rect, FrameZone::Terminal { idx }),
Surface::TextDisplay { rect, .. } => (*rect, FrameZone::TextDisplay { idx }),
Surface::MultiSectionView { rect, .. } => (*rect, FrameZone::MultiSectionView { idx }),
Surface::Tree { rect, .. } => (*rect, FrameZone::Tree { idx }),
Surface::List { rect, .. } => (*rect, FrameZone::List { idx }),
Surface::Form { rect, .. } => (*rect, FrameZone::Form { idx }),
Surface::MenuBar { rect, .. } => (*rect, FrameZone::MenuBar { idx }),
Surface::Split { rect, .. } => (*rect, FrameZone::Split { idx }),
Surface::Panel { rect, .. } => (*rect, FrameZone::Panel { idx }),
Surface::Scrollbar { rect, .. } => (*rect, FrameZone::Scrollbar { idx }),
Surface::Palette { rect, .. } => (*rect, FrameZone::Palette { idx }),
Surface::Tooltip { layout, .. } => (layout.bounds, FrameZone::Tooltip { idx }),
Surface::ContextMenu { layout, .. } => (layout.bounds, FrameZone::ContextMenu { idx }),
Surface::Dialog { layout, .. } => (layout.bounds, FrameZone::Dialog { idx }),
Surface::Float { layout, .. } => (layout.bounds, FrameZone::Float { idx }),
Surface::Completions { layout, .. } => (layout.bounds, FrameZone::Completions { idx }),
Surface::FindReplace { rect, .. } => (*rect, FrameZone::FindReplace { idx }),
Surface::RichTextPopup { layout, .. } => {
(layout.bounds, FrameZone::RichTextPopup { idx })
}
Surface::Toast { rect, .. } => (*rect, FrameZone::Toast { idx }),
Surface::DataTable { rect, .. } => (*rect, FrameZone::DataTable { idx }),
Surface::Chart { rect, .. } => (*rect, FrameZone::Chart { idx }),
Surface::Canvas { rect, .. } => (*rect, FrameZone::Canvas { idx }),
Surface::Board { rect, .. } => (*rect, FrameZone::Board { idx }),
Surface::CommandCenter { rect, .. } => (*rect, FrameZone::CommandCenter { idx }),
Surface::DiffView { rect, .. } => (*rect, FrameZone::DiffView { idx }),
Surface::DropOverlay { overlay } => (overlay.bounds(), FrameZone::DropOverlay { idx }),
Surface::Image { rect, .. } => (*rect, FrameZone::Image { idx }),
Surface::MessageList { rect, .. } => (*rect, FrameZone::MessageList { idx }),
Surface::Minimap { rect, .. } => (*rect, FrameZone::Minimap { idx }),
Surface::PipelineView { rect, .. } => (*rect, FrameZone::PipelineView { idx }),
Surface::Progress { rect, .. } => (*rect, FrameZone::Progress { idx }),
Surface::SidebarPanel { rect, .. } => (*rect, FrameZone::SidebarPanel { idx }),
Surface::Spinner { rect, .. } => (*rect, FrameZone::Spinner { idx }),
Surface::SplitTree { rect, .. } => (*rect, FrameZone::SplitTree { idx }),
Surface::TextInput { rect, .. } => (*rect, FrameZone::TextInput { idx }),
Surface::Toolbar { rect, .. } => (*rect, FrameZone::Toolbar { idx }),
}
}
}
impl Default for ScreenLayout {
fn default() -> Self {
Self::new()
}
}
#[non_exhaustive]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum OwnedContent {
Editor(Editor),
TabBar(TabBar),
StatusBar(StatusBar),
ActivityBar(ActivityBar),
CommandLine(CommandLine),
Terminal(Terminal),
TextDisplay(TextDisplay),
MultiSectionView(MultiSectionView),
Tree(TreeView),
List(ListView),
Form(Form),
MenuBar(MenuBar),
Split(Split),
Panel(Panel),
Scrollbar(Scrollbar),
Palette(Palette),
Tooltip(Tooltip),
ContextMenu(ContextMenu),
Dialog(Dialog),
Float(Float),
Completions(Completions),
RichTextPopup(RichTextPopup),
Toast(ToastOverlay),
DataTable(DataTable),
Chart(Chart),
Canvas(Canvas),
Board(BoardModel),
CommandCenter(CommandCenter),
DiffView(DiffView),
Image(Image),
MessageList(MessageList),
Minimap(Minimap),
PipelineView(PipelineView),
Progress(ProgressBar),
SidebarPanel(SidebarPanel),
Spinner(Spinner),
SplitTree(SplitTree),
TextInput(TextInput),
Toolbar(Toolbar),
DropOverlay,
FindReplace,
Opaque,
}
impl OwnedContent {
fn draw(&self, rect: Rect, backend: &mut dyn Backend) {
match self {
OwnedContent::Editor(editor) => {
backend.draw_editor(rect, editor);
}
OwnedContent::TabBar(bar) => {
backend.draw_tab_bar(rect, bar, None);
}
OwnedContent::StatusBar(bar) => {
let interaction = crate::interaction::InteractionState::from_parts(None, None);
backend.draw_status_bar_interactive(rect, bar, &interaction);
}
OwnedContent::ActivityBar(bar) => {
backend.draw_activity_bar(rect, bar, None);
}
OwnedContent::CommandLine(cmd) => backend.draw_command_line(rect, cmd),
OwnedContent::Terminal(term) => backend.draw_terminal(rect, term),
OwnedContent::TextDisplay(td) => backend.draw_text_display(rect, td),
OwnedContent::MultiSectionView(view) => backend.draw_multi_section_view(rect, view),
OwnedContent::Tree(tree) => backend.draw_tree(rect, tree),
OwnedContent::List(list) => backend.draw_list(rect, list),
OwnedContent::Form(form) => backend.draw_form(rect, form),
OwnedContent::MenuBar(bar) => {
backend.draw_menu_bar(rect, bar);
}
OwnedContent::Split(split) => {
backend.draw_split(rect, split);
}
OwnedContent::Panel(panel) => {
backend.draw_panel(rect, panel);
}
OwnedContent::Scrollbar(sb) => backend.draw_scrollbar(rect, sb),
OwnedContent::Palette(palette) => backend.draw_palette(rect, palette),
OwnedContent::Toast(stack) => {
backend.draw_toast_overlay(rect, stack);
}
OwnedContent::DataTable(table) => {
backend.draw_data_table(rect, table, None);
}
OwnedContent::Chart(chart) => {
backend.draw_chart(rect, chart, None, None);
}
OwnedContent::Canvas(canvas) => {
backend.draw_canvas(rect, canvas);
}
OwnedContent::Board(model) => {
backend.draw_board(rect, model);
}
OwnedContent::CommandCenter(cc) => {
backend.draw_command_center(rect, cc);
}
OwnedContent::DiffView(view) => {
backend.draw_diff_view(rect, view);
}
OwnedContent::Image(image) => {
backend.draw_image(rect, image);
}
OwnedContent::MessageList(list) => backend.draw_message_list(rect, list),
OwnedContent::Minimap(minimap) => {
backend.draw_minimap(rect, minimap);
}
OwnedContent::PipelineView(view) => {
backend.draw_pipeline_view(rect, view);
}
OwnedContent::Progress(bar) => {
backend.draw_progress(rect, bar);
}
OwnedContent::SidebarPanel(panel) => {
let interaction = crate::interaction::InteractionState::from_parts(None, None);
backend.draw_sidebar_panel_interactive(rect, panel, &interaction);
}
OwnedContent::Spinner(spinner) => {
backend.draw_spinner(rect, spinner);
}
OwnedContent::SplitTree(tree) => {
backend.draw_split_tree(rect, tree);
}
OwnedContent::TextInput(ti) => {
backend.draw_text_input(rect, ti);
}
OwnedContent::Toolbar(bar) => {
let interaction = crate::interaction::InteractionState::from_parts(None, None);
backend.draw_toolbar_interactive(rect, bar, &interaction);
}
OwnedContent::Tooltip(_)
| OwnedContent::ContextMenu(_)
| OwnedContent::Dialog(_)
| OwnedContent::Float(_)
| OwnedContent::Completions(_)
| OwnedContent::RichTextPopup(_)
| OwnedContent::DropOverlay
| OwnedContent::FindReplace
| OwnedContent::Opaque => {}
}
}
fn zone(&self, idx: usize) -> FrameZone {
match self {
OwnedContent::Editor(_) => FrameZone::Editor { idx },
OwnedContent::TabBar(_) => FrameZone::TabBar { idx },
OwnedContent::StatusBar(_) => FrameZone::StatusBar { idx },
OwnedContent::ActivityBar(_) => FrameZone::ActivityBar { idx },
OwnedContent::CommandLine(_) => FrameZone::CommandLine { idx },
OwnedContent::Terminal(_) => FrameZone::Terminal { idx },
OwnedContent::TextDisplay(_) => FrameZone::TextDisplay { idx },
OwnedContent::MultiSectionView(_) => FrameZone::MultiSectionView { idx },
OwnedContent::Tree(_) => FrameZone::Tree { idx },
OwnedContent::List(_) => FrameZone::List { idx },
OwnedContent::Form(_) => FrameZone::Form { idx },
OwnedContent::MenuBar(_) => FrameZone::MenuBar { idx },
OwnedContent::Split(_) => FrameZone::Split { idx },
OwnedContent::Panel(_) => FrameZone::Panel { idx },
OwnedContent::Scrollbar(_) => FrameZone::Scrollbar { idx },
OwnedContent::Palette(_) => FrameZone::Palette { idx },
OwnedContent::Tooltip(_) => FrameZone::Tooltip { idx },
OwnedContent::ContextMenu(_) => FrameZone::ContextMenu { idx },
OwnedContent::Dialog(_) => FrameZone::Dialog { idx },
OwnedContent::Float(_) => FrameZone::Float { idx },
OwnedContent::Completions(_) => FrameZone::Completions { idx },
OwnedContent::RichTextPopup(_) => FrameZone::RichTextPopup { idx },
OwnedContent::Toast(_) => FrameZone::Toast { idx },
OwnedContent::DataTable(_) => FrameZone::DataTable { idx },
OwnedContent::Chart(_) => FrameZone::Chart { idx },
OwnedContent::Canvas(_) => FrameZone::Canvas { idx },
OwnedContent::Board(_) => FrameZone::Board { idx },
OwnedContent::CommandCenter(_) => FrameZone::CommandCenter { idx },
OwnedContent::DiffView(_) => FrameZone::DiffView { idx },
OwnedContent::Image(_) => FrameZone::Image { idx },
OwnedContent::MessageList(_) => FrameZone::MessageList { idx },
OwnedContent::Minimap(_) => FrameZone::Minimap { idx },
OwnedContent::PipelineView(_) => FrameZone::PipelineView { idx },
OwnedContent::Progress(_) => FrameZone::Progress { idx },
OwnedContent::SidebarPanel(_) => FrameZone::SidebarPanel { idx },
OwnedContent::Spinner(_) => FrameZone::Spinner { idx },
OwnedContent::SplitTree(_) => FrameZone::SplitTree { idx },
OwnedContent::TextInput(_) => FrameZone::TextInput { idx },
OwnedContent::Toolbar(_) => FrameZone::Toolbar { idx },
OwnedContent::DropOverlay => FrameZone::DropOverlay { idx },
OwnedContent::FindReplace => FrameZone::FindReplace { idx },
OwnedContent::Opaque => FrameZone::Empty,
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct OwnedNode {
pub idx: usize,
pub rect: Rect,
pub widget_id: Option<WidgetId>,
pub a11y: Option<crate::a11y::A11yInfo>,
pub content: OwnedContent,
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct OwnedFrame {
nodes: Vec<OwnedNode>,
}
impl OwnedFrame {
pub fn new() -> Self {
Self::default()
}
pub fn from_layout(layout: &ScreenLayout) -> Self {
let nodes = layout
.surfaces
.iter()
.enumerate()
.map(|(idx, surface)| {
let (rect, widget_id, content) = capture_surface(surface);
OwnedNode {
idx,
rect,
widget_id,
a11y: None,
content,
}
})
.collect();
Self { nodes }
}
pub fn nodes(&self) -> &[OwnedNode] {
&self.nodes
}
pub fn nodes_mut(&mut self) -> &mut [OwnedNode] {
&mut self.nodes
}
pub fn draw(&self, backend: &mut dyn Backend) -> FrameHitMap {
let mut hit_map = FrameHitMap::new();
for node in &self.nodes {
node.content.draw(node.rect, backend);
hit_map.push(node.rect, node.content.zone(node.idx));
}
hit_map
}
pub fn hit_map(&self) -> FrameHitMap {
let mut hit_map = FrameHitMap::new();
for node in &self.nodes {
hit_map.push(node.rect, node.content.zone(node.idx));
}
hit_map
}
}
fn capture_surface(surface: &Surface) -> (Rect, Option<WidgetId>, OwnedContent) {
match surface {
Surface::Editor { rect, editor } => {
(*rect, Some(editor.id), OwnedContent::Editor(editor.clone()))
}
Surface::TabBar { rect, bar, .. } => {
(*rect, Some(bar.id), OwnedContent::TabBar(bar.clone()))
}
Surface::StatusBar { rect, bar, .. } => {
(*rect, Some(bar.id), OwnedContent::StatusBar(bar.clone()))
}
Surface::ActivityBar { rect, bar, .. } => {
(*rect, Some(bar.id), OwnedContent::ActivityBar(bar.clone()))
}
Surface::CommandLine { rect, cmd } => {
(*rect, Some(cmd.id), OwnedContent::CommandLine(cmd.clone()))
}
Surface::Terminal { rect, term } => {
(*rect, Some(term.id), OwnedContent::Terminal(term.clone()))
}
Surface::TextDisplay { rect, td } => {
(*rect, Some(td.id), OwnedContent::TextDisplay(td.clone()))
}
Surface::MultiSectionView { rect, view } => (
*rect,
Some(view.id),
OwnedContent::MultiSectionView(view.clone()),
),
Surface::Tree { rect, tree } => (*rect, Some(tree.id), OwnedContent::Tree(tree.clone())),
Surface::List { rect, list } => (*rect, Some(list.id), OwnedContent::List(list.clone())),
Surface::Form { rect, form } => (*rect, Some(form.id), OwnedContent::Form(form.clone())),
Surface::MenuBar { rect, bar } => (*rect, Some(bar.id), OwnedContent::MenuBar(bar.clone())),
Surface::Split { rect, split } => (*rect, None, OwnedContent::Split(split.clone())),
Surface::Panel { rect, panel } => {
(*rect, Some(panel.id), OwnedContent::Panel(panel.clone()))
}
Surface::Scrollbar { rect, sb } => (*rect, None, OwnedContent::Scrollbar(sb.clone())),
Surface::Palette { rect, palette } => (
*rect,
Some(palette.id),
OwnedContent::Palette(palette.clone()),
),
Surface::Tooltip { tooltip, layout } => {
(layout.bounds, None, OwnedContent::Tooltip(tooltip.clone()))
}
Surface::ContextMenu { menu, layout } => {
(layout.bounds, None, OwnedContent::ContextMenu(menu.clone()))
}
Surface::Dialog { dialog, layout } => {
(layout.bounds, None, OwnedContent::Dialog(dialog.clone()))
}
Surface::Float { float, layout } => {
(layout.bounds, None, OwnedContent::Float(float.clone()))
}
Surface::Completions {
completions,
layout,
} => (
layout.bounds,
None,
OwnedContent::Completions(completions.clone()),
),
Surface::FindReplace { rect, .. } => (*rect, None, OwnedContent::FindReplace),
Surface::RichTextPopup { popup, layout } => (
layout.bounds,
None,
OwnedContent::RichTextPopup(popup.clone()),
),
Surface::Toast { rect, stack } => (*rect, None, OwnedContent::Toast(stack.clone())),
Surface::DataTable { rect, table, .. } => (
*rect,
Some(table.id),
OwnedContent::DataTable(table.clone()),
),
Surface::Chart { rect, chart, .. } => {
(*rect, Some(chart.id), OwnedContent::Chart(chart.clone()))
}
Surface::Canvas { rect, canvas } => (*rect, None, OwnedContent::Canvas(canvas.clone())),
Surface::Board { rect, model } => {
(*rect, Some(model.id), OwnedContent::Board(model.clone()))
}
Surface::CommandCenter { rect, cc } => {
(*rect, Some(cc.id), OwnedContent::CommandCenter(cc.clone()))
}
Surface::DiffView { rect, view } => {
(*rect, Some(view.id), OwnedContent::DiffView(view.clone()))
}
Surface::DropOverlay { overlay } => (overlay.bounds(), None, OwnedContent::DropOverlay),
Surface::Image { rect, image } => (*rect, None, OwnedContent::Image(image.clone())),
Surface::MessageList { rect, list } => (
*rect,
Some(list.id),
OwnedContent::MessageList(list.clone()),
),
Surface::Minimap { rect, minimap } => (
*rect,
Some(minimap.id),
OwnedContent::Minimap(minimap.clone()),
),
Surface::PipelineView { rect, view } => (
*rect,
Some(view.id),
OwnedContent::PipelineView(view.clone()),
),
Surface::Progress { rect, bar } => (
*rect,
if bar.cancellable { Some(bar.id) } else { None },
OwnedContent::Progress(bar.clone()),
),
Surface::SidebarPanel { rect, panel, .. } => (
*rect,
Some(panel.id),
OwnedContent::SidebarPanel(panel.clone()),
),
Surface::Spinner { rect, spinner } => (*rect, None, OwnedContent::Spinner(spinner.clone())),
Surface::SplitTree { rect, tree } => (*rect, None, OwnedContent::SplitTree(tree.clone())),
Surface::TextInput { rect, ti } => {
(*rect, Some(ti.id), OwnedContent::TextInput(ti.clone()))
}
Surface::Toolbar { rect, bar, .. } => {
(*rect, Some(bar.id), OwnedContent::Toolbar(bar.clone()))
}
}
}
pub trait FrameRung: Copy + Eq + std::fmt::Debug + 'static {
fn z_order() -> &'static [Self]
where
Self: Sized;
fn rank(&self) -> usize
where
Self: Sized,
{
Self::z_order()
.iter()
.position(|r| r == self)
.unwrap_or_else(|| panic!("{self:?} is missing from FrameRung::z_order()"))
}
}
pub struct FramePresence<R> {
present: Vec<(R, bool)>,
}
impl<R: FrameRung> FramePresence<R> {
pub fn from_fn(predicate: impl Fn(R) -> bool) -> Self {
let present = R::z_order().iter().map(|&r| (r, predicate(r))).collect();
Self { present }
}
pub fn is_present(&self, rung: R) -> bool {
self.present
.iter()
.any(|(r, present)| *r == rung && *present)
}
pub fn present_rungs(&self) -> impl Iterator<Item = R> + '_ {
self.present.iter().filter(|(_, p)| *p).map(|(r, _)| *r)
}
}
pub fn compose_frame<R: FrameRung>(
presence: &FramePresence<R>,
mut paint: impl FnMut(R),
) -> Vec<R> {
let mut visited = Vec::new();
for rung in presence.present_rungs() {
paint(rung);
visited.push(rung);
}
visited
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FrameOrderViolation<R> {
pub before: R,
pub after: R,
}
pub fn check_frame_order<R: FrameRung>(visited: &[R]) -> Result<(), FrameOrderViolation<R>> {
let mut last: Option<(R, usize)> = None;
for &rung in visited {
let rank = rung.rank();
if let Some((before, before_rank)) = last {
if rank < before_rank {
return Err(FrameOrderViolation {
before,
after: rung,
});
}
}
last = Some((rung, rank));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::primitives::tree::TreeRow;
use crate::types::{SelectionMode, StyledText, TreeStyle};
#[test]
fn hit_test_returns_highest_z_surface() {
let mut map = FrameHitMap::new();
map.push(
Rect::new(0.0, 0.0, 100.0, 100.0),
FrameZone::Editor { idx: 0 },
);
map.push(
Rect::new(0.0, 0.0, 50.0, 50.0),
FrameZone::Palette { idx: 1 },
);
assert_eq!(map.hit_test(25.0, 25.0), FrameZone::Palette { idx: 1 });
assert_eq!(map.hit_test(75.0, 75.0), FrameZone::Editor { idx: 0 });
assert_eq!(map.hit_test(150.0, 150.0), FrameZone::Empty);
}
#[test]
fn hit_test_empty_map() {
let map = FrameHitMap::new();
assert_eq!(map.hit_test(10.0, 10.0), FrameZone::Empty);
assert!(map.is_empty());
}
#[test]
fn multiple_overlapping_zones_last_wins() {
let mut map = FrameHitMap::new();
map.push(
Rect::new(0.0, 0.0, 80.0, 24.0),
FrameZone::Editor { idx: 0 },
);
map.push(Rect::new(0.0, 0.0, 80.0, 1.0), FrameZone::TabBar { idx: 1 });
map.push(
Rect::new(0.0, 23.0, 80.0, 1.0),
FrameZone::StatusBar { idx: 2 },
);
assert_eq!(map.hit_test(40.0, 0.5), FrameZone::TabBar { idx: 1 });
assert_eq!(map.hit_test(40.0, 12.0), FrameZone::Editor { idx: 0 });
assert_eq!(map.hit_test(40.0, 23.5), FrameZone::StatusBar { idx: 2 });
}
#[test]
fn screen_layout_default_is_empty() {
let layout = ScreenLayout::default();
assert_eq!(layout.surfaces.len(), 0);
}
fn sample_tree(id: &str, scroll_offset: usize) -> TreeView {
TreeView {
id: WidgetId::new(id),
rows: vec![TreeRow {
path: vec![0],
indent: 0,
icon: None,
text: StyledText::plain("src/"),
badge: None,
is_expanded: Some(true),
decoration: Default::default(),
edit: None,
}],
selection_mode: SelectionMode::Single,
selected_path: Some(vec![0]),
scroll_offset,
style: TreeStyle::default(),
has_focus: false,
}
}
#[test]
fn tree_frame_carries_handles_with_matching_rects() {
let mut layout = ScreenLayout::new();
layout.push(Surface::Tree {
rect: Rect::new(0.0, 0.0, 40.0, 10.0),
tree: sample_tree("tree:explorer", 0),
});
let frame = layout.tree_frame();
assert_eq!(frame.len(), 1);
let (handle, rect) = frame[0];
assert_eq!(rect, Rect::new(0.0, 0.0, 40.0, 10.0));
assert_eq!(
layout.tree_store.get(handle).map(|t| &t.id),
Some(&WidgetId::new("tree:explorer"))
);
}
#[test]
fn tree_frame_reuses_the_same_handle_across_frames_without_rebuilding() {
let mut layout = ScreenLayout::new();
layout.push(Surface::Tree {
rect: Rect::new(0.0, 0.0, 40.0, 10.0),
tree: sample_tree("tree:explorer", 0),
});
let first = layout.tree_frame();
let first_handle = first[0].0;
assert_eq!(layout.tree_store.len(), 1);
let second = layout.tree_frame();
assert_eq!(second[0].0, first_handle);
assert_eq!(layout.tree_store.len(), 1);
layout.surfaces_mut()[0] = Surface::Tree {
rect: Rect::new(0.0, 0.0, 40.0, 10.0),
tree: sample_tree("tree:explorer", 5),
};
let third = layout.tree_frame();
assert_eq!(third[0].0, first_handle);
assert_eq!(layout.tree_store.len(), 1);
assert_eq!(
layout.tree_store.get(third[0].0).map(|t| t.scroll_offset),
Some(5)
);
layout.push(Surface::Tree {
rect: Rect::new(0.0, 10.0, 40.0, 10.0),
tree: sample_tree("tree:outline", 0),
});
let fourth = layout.tree_frame();
assert_eq!(fourth.len(), 2);
assert_eq!(layout.tree_store.len(), 2);
}
#[test]
fn tree_frame_evicts_handles_for_ids_no_longer_pushed() {
let mut layout = ScreenLayout::new();
layout.push(Surface::Tree {
rect: Rect::new(0.0, 0.0, 40.0, 10.0),
tree: sample_tree("tree:explorer", 0),
});
let first = layout.tree_frame();
let stale_handle = first[0].0;
assert_eq!(layout.tree_store.len(), 1);
layout.surfaces_mut()[0] = Surface::Tree {
rect: Rect::new(0.0, 0.0, 40.0, 10.0),
tree: sample_tree("tree:outline", 0),
};
let second = layout.tree_frame();
assert_eq!(layout.tree_store.len(), 1);
assert!(layout.tree_store.get(stale_handle).is_none());
assert_eq!(
layout.tree_store.get(second[0].0).map(|t| &t.id),
Some(&WidgetId::new("tree:outline"))
);
let mut empty_layout = ScreenLayout::new();
empty_layout.push(Surface::Tree {
rect: Rect::new(0.0, 0.0, 40.0, 10.0),
tree: sample_tree("tree:explorer", 0),
});
empty_layout.tree_frame();
assert_eq!(empty_layout.tree_store.len(), 1);
empty_layout.surfaces_mut()[0] = Surface::Editor {
rect: Rect::new(0.0, 0.0, 40.0, 10.0),
editor: Editor {
id: "ed".into(),
rect: Rect::new(0.0, 0.0, 40.0, 10.0),
lines: Vec::new(),
cursor: None,
extra_cursors: Vec::new(),
selection: None,
extra_selections: Vec::new(),
yank_highlight: None,
scroll_top: 0,
scroll_left: 0,
total_lines: 0,
max_col: 0,
gutter_char_width: 0,
is_active: true,
show_active_bg: false,
has_git_diff: false,
has_breakpoints: false,
diagnostic_gutter: std::collections::HashMap::new(),
code_action_lines: std::collections::HashSet::new(),
bracket_match_positions: Vec::new(),
active_indent_col: None,
tabstop: 4,
cursorline: false,
lightbulb_glyph: '\0',
},
};
empty_layout.tree_frame();
assert_eq!(empty_layout.tree_store.len(), 0);
}
#[test]
fn hit_map_registers_zones_without_a_backend() {
let editor = Editor {
id: "ed".into(),
rect: Rect::new(0.0, 0.0, 100.0, 100.0),
lines: Vec::new(),
cursor: None,
extra_cursors: Vec::new(),
selection: None,
extra_selections: Vec::new(),
yank_highlight: None,
scroll_top: 0,
scroll_left: 0,
total_lines: 0,
max_col: 0,
gutter_char_width: 0,
is_active: true,
show_active_bg: false,
has_git_diff: false,
has_breakpoints: false,
diagnostic_gutter: std::collections::HashMap::new(),
code_action_lines: std::collections::HashSet::new(),
bracket_match_positions: Vec::new(),
active_indent_col: None,
tabstop: 4,
cursorline: false,
lightbulb_glyph: '\0',
};
let palette = Palette {
id: "pal".into(),
title: String::new(),
query: String::new(),
query_cursor: 0,
items: Vec::new(),
selected_idx: 0,
scroll_offset: 0,
total_count: 0,
has_focus: false,
show_query: true,
create_label: None,
preview: None,
mode: crate::primitives::palette::PaletteMode::List,
};
let mut layout = ScreenLayout::new();
layout.push(Surface::Editor {
rect: Rect::new(0.0, 0.0, 100.0, 100.0),
editor,
});
layout.push(Surface::Palette {
rect: Rect::new(0.0, 0.0, 50.0, 50.0),
palette,
});
let hit_map = layout.hit_map();
assert_eq!(hit_map.len(), 2);
assert_eq!(hit_map.hit_test(25.0, 25.0), FrameZone::Palette { idx: 1 });
assert_eq!(hit_map.hit_test(75.0, 75.0), FrameZone::Editor { idx: 0 });
assert_eq!(hit_map.hit_test(150.0, 150.0), FrameZone::Empty);
}
#[test]
fn surface_owns_its_primitive_and_outlives_the_source() {
let rect = Rect::new(2.0, 3.0, 20.0, 8.0);
let surface = {
let list = MessageList {
id: "ml".into(),
rows: Vec::new(),
scroll_top: 0,
};
Surface::MessageList { rect, list }
};
let mut layout = ScreenLayout::new();
layout.push(surface);
let hit_map = layout.hit_map();
assert_eq!(hit_map.len(), 1);
assert_eq!(hit_map.zones()[0].0, rect);
assert_eq!(
hit_map.hit_test(3.0, 4.0),
FrameZone::MessageList { idx: 0 }
);
assert_eq!(hit_map.hit_test(100.0, 100.0), FrameZone::Empty);
}
#[test]
fn screen_layout_returned_from_a_function_draws_and_hit_maps() {
use crate::testing::RecordingBackend;
fn build() -> ScreenLayout {
let list = MessageList {
id: "ml".into(),
rows: Vec::new(),
scroll_top: 0,
};
let mut layout = ScreenLayout::new();
layout.push(Surface::MessageList {
rect: Rect::new(0.0, 0.0, 40.0, 10.0),
list,
});
layout
}
let layout = build();
let mut backend = RecordingBackend::new();
let drawn = layout.draw(&mut backend);
assert!(backend.calls.contains(&"draw_message_list"));
assert_eq!(drawn, layout.hit_map());
assert_eq!(drawn.hit_test(1.0, 1.0), FrameZone::MessageList { idx: 0 });
}
#[test]
fn tab_stops_sorts_by_reading_order_not_push_order() {
let editor = Editor {
id: "ed".into(),
rect: Rect::new(0.0, 10.0, 100.0, 20.0),
lines: Vec::new(),
cursor: None,
extra_cursors: Vec::new(),
selection: None,
extra_selections: Vec::new(),
yank_highlight: None,
scroll_top: 0,
scroll_left: 0,
total_lines: 0,
max_col: 0,
gutter_char_width: 0,
is_active: true,
show_active_bg: false,
has_git_diff: false,
has_breakpoints: false,
diagnostic_gutter: std::collections::HashMap::new(),
code_action_lines: std::collections::HashSet::new(),
bracket_match_positions: Vec::new(),
active_indent_col: None,
tabstop: 4,
cursorline: false,
lightbulb_glyph: '\0',
};
let palette = Palette {
id: "pal".into(),
title: String::new(),
query: String::new(),
query_cursor: 0,
items: Vec::new(),
selected_idx: 0,
scroll_offset: 0,
total_count: 0,
has_focus: false,
show_query: true,
create_label: None,
preview: None,
mode: crate::primitives::palette::PaletteMode::List,
};
let split = crate::primitives::split::Split {
id: "split".into(),
direction: crate::primitives::split::SplitDirection::Horizontal,
ratio: 0.5,
first_min: 0.0,
second_min: 0.0,
};
let mut layout = ScreenLayout::new();
layout.push(Surface::Editor {
rect: editor.rect,
editor,
});
layout.push(Surface::Palette {
rect: Rect::new(0.0, 0.0, 50.0, 5.0),
palette,
});
layout.push(Surface::Split {
rect: Rect::new(0.0, 30.0, 100.0, 1.0),
split,
});
let stops = layout.tab_stops();
assert_eq!(
stops.iter().map(|(id, _)| id.as_str()).collect::<Vec<_>>(),
vec!["pal", "ed"],
"palette (y=0) must sort before the editor (y=10), regardless of push order"
);
assert!(
!stops.iter().any(|(id, _)| id.as_str() == "split"),
"Split is structural chrome, not a tab stop"
);
}
#[test]
fn surface_gap_closed_for_all_fourteen_primitives() {
let board = crate::primitives::board::BoardModel {
id: "board".into(),
columns: Vec::new(),
selected_card_id: None,
col_scroll_offset: 0,
};
let cc = CommandCenter {
id: "cc".into(),
back_enabled: false,
forward_enabled: false,
search_label: String::new(),
};
let diff = DiffView {
id: "diff".into(),
left: String::new(),
right: String::new(),
left_label: None,
right_label: None,
hunks: Vec::new(),
mode: Default::default(),
editability: Default::default(),
scroll_offset: 0,
focused_pane: Default::default(),
has_focus: false,
};
let overlay = DropOverlay {
highlight: Some(Rect::new(1.0, 1.0, 2.0, 2.0)),
insertion_bar: None,
ghost_position: None,
};
let image = Image {
id: "img".into(),
source: crate::primitives::image::ImageSource::Path(std::path::PathBuf::new()),
intrinsic_size: None,
fit: Default::default(),
fallback_text: String::new(),
};
let message_list = MessageList {
id: "ml".into(),
rows: Vec::new(),
scroll_top: 0,
};
let minimap = Minimap {
id: "mm".into(),
lines: Vec::new(),
syntax_spans: Vec::new(),
visible_row_start: 0,
visible_row_count: 0,
total_buffer_lines: 0,
};
let pipeline = PipelineView {
id: "pv".into(),
stages: Vec::new(),
focused_stage: None,
};
let progress = ProgressBar {
id: "pb".into(),
label: String::new(),
value: None,
frame_idx: 0,
cancellable: false,
accent: None,
};
let sidebar = SidebarPanel {
id: "sp".into(),
toolbar: None,
toolbar_height: None,
};
let spinner = Spinner {
id: "sn".into(),
label: String::new(),
frame_idx: 0,
accent: None,
};
let split_tree = SplitTree::Leaf("leaf".into());
let text_input = TextInput {
id: "ti".into(),
lines: vec![String::new()],
cursor_line: 0,
cursor_col: 0,
placeholder: None,
scroll_offset: 0,
scroll_col: 0,
has_focus: false,
};
let toolbar = Toolbar {
id: "tb".into(),
buttons: Vec::new(),
bg: None,
focused_index: None,
};
let rect = Rect::new(0.0, 0.0, 10.0, 10.0);
let mut layout = ScreenLayout::new();
layout.push(Surface::Board { rect, model: board });
layout.push(Surface::CommandCenter { rect, cc });
layout.push(Surface::DiffView { rect, view: diff });
layout.push(Surface::DropOverlay {
overlay: overlay.clone(),
});
layout.push(Surface::Image { rect, image });
layout.push(Surface::MessageList {
rect,
list: message_list,
});
layout.push(Surface::Minimap { rect, minimap });
layout.push(Surface::PipelineView {
rect,
view: pipeline,
});
layout.push(Surface::Progress {
rect,
bar: progress,
});
layout.push(Surface::SidebarPanel {
rect,
panel: sidebar,
hovered: None,
pressed: None,
});
layout.push(Surface::Spinner { rect, spinner });
layout.push(Surface::SplitTree {
rect,
tree: split_tree,
});
layout.push(Surface::TextInput {
rect,
ti: text_input,
});
layout.push(Surface::Toolbar {
rect,
bar: toolbar,
hovered: None,
pressed: None,
});
let hit_map = layout.hit_map();
assert_eq!(hit_map.len(), 14);
let zones: Vec<&FrameZone> = hit_map.zones().iter().map(|(_, z)| z).collect();
assert_eq!(zones[0], &FrameZone::Board { idx: 0 });
assert_eq!(zones[1], &FrameZone::CommandCenter { idx: 1 });
assert_eq!(zones[2], &FrameZone::DiffView { idx: 2 });
assert_eq!(zones[3], &FrameZone::DropOverlay { idx: 3 });
assert_eq!(zones[4], &FrameZone::Image { idx: 4 });
assert_eq!(zones[5], &FrameZone::MessageList { idx: 5 });
assert_eq!(zones[6], &FrameZone::Minimap { idx: 6 });
assert_eq!(zones[7], &FrameZone::PipelineView { idx: 7 });
assert_eq!(zones[8], &FrameZone::Progress { idx: 8 });
assert_eq!(zones[9], &FrameZone::SidebarPanel { idx: 9 });
assert_eq!(zones[10], &FrameZone::Spinner { idx: 10 });
assert_eq!(zones[11], &FrameZone::SplitTree { idx: 11 });
assert_eq!(zones[12], &FrameZone::TextInput { idx: 12 });
assert_eq!(zones[13], &FrameZone::Toolbar { idx: 13 });
assert_eq!(hit_map.zones()[3].0, overlay.bounds());
let stops = layout.tab_stops();
let stop_ids: Vec<&str> = stops.iter().map(|(id, _)| id.as_str()).collect();
for id in ["board", "cc", "diff", "ml", "mm", "pv", "sp", "ti", "tb"] {
assert!(stop_ids.contains(&id), "{id} should be a tab stop");
}
for id in ["img", "sn", "pb", "leaf"] {
assert!(!stop_ids.contains(&id), "{id} should not be a tab stop");
}
assert_eq!(stop_ids.len(), 9);
}
#[test]
fn cancellable_progress_is_a_tab_stop() {
let progress = ProgressBar {
id: "pb".into(),
label: String::new(),
value: None,
frame_idx: 0,
cancellable: true,
accent: None,
};
let rect = Rect::new(0.0, 0.0, 10.0, 10.0);
let mut layout = ScreenLayout::new();
layout.push(Surface::Progress {
rect,
bar: progress,
});
let stops = layout.tab_stops();
assert_eq!(stops.len(), 1);
assert_eq!(stops[0].0.as_str(), "pb");
}
#[test]
fn surface_float_dispatches_to_draw_float_and_is_excluded_from_tab_stops() {
use crate::layout::{Anchor, Side};
use crate::testing::RecordingBackend;
use crate::{Float, FloatMeasure};
let editor = Editor {
id: "ed".into(),
rect: Rect::new(0.0, 0.0, 100.0, 100.0),
lines: Vec::new(),
cursor: None,
extra_cursors: Vec::new(),
selection: None,
extra_selections: Vec::new(),
yank_highlight: None,
scroll_top: 0,
scroll_left: 0,
total_lines: 0,
max_col: 0,
gutter_char_width: 0,
is_active: true,
show_active_bg: false,
has_git_diff: false,
has_breakpoints: false,
diagnostic_gutter: std::collections::HashMap::new(),
code_action_lines: std::collections::HashSet::new(),
bracket_match_positions: Vec::new(),
active_indent_col: None,
tabstop: 4,
cursorline: false,
lightbulb_glyph: '\0',
};
let float = Float::new(
WidgetId::new("f"),
Anchor::new(Rect::new(0.0, 0.0, 5.0, 1.0), Side::Bottom),
);
let float_layout = float.layout(
Rect::new(0.0, 0.0, 80.0, 24.0),
FloatMeasure::new(10.0, 4.0),
);
let mut layout = ScreenLayout::new();
layout.push(Surface::Editor {
rect: Rect::new(0.0, 0.0, 100.0, 100.0),
editor,
});
layout.push(Surface::Float {
float,
layout: float_layout,
});
let mut backend = RecordingBackend::new();
layout.draw(&mut backend);
assert!(backend.calls.contains(&"draw_float"));
let stops = layout.tab_stops();
assert_eq!(
stops.len(),
1,
"the Float surface must not become a Tab stop — it owns its \
own focus story via ModalStack::top_focusable"
);
assert_eq!(stops[0].0.as_str(), "ed");
}
#[test]
fn frame_hit_map_serde_round_trip() {
let mut map = FrameHitMap::new();
map.push(Rect::new(0.0, 0.0, 10.0, 10.0), FrameZone::Board { idx: 0 });
map.push(Rect::new(0.0, 0.0, 5.0, 5.0), FrameZone::Toolbar { idx: 1 });
let json = serde_json::to_string(&map).expect("FrameHitMap serializes");
let restored: FrameHitMap = serde_json::from_str(&json).expect("FrameHitMap deserializes");
assert_eq!(restored, map);
assert_eq!(restored.hit_test(1.0, 1.0), FrameZone::Toolbar { idx: 1 });
let rebuilt = FrameHitMap::from_zones(map.zones().to_vec());
assert_eq!(rebuilt, map);
}
#[test]
fn owned_frame_serde_round_trip_preserves_hit_map() {
let editor = Editor {
id: "ed".into(),
rect: Rect::new(0.0, 0.0, 100.0, 100.0),
lines: Vec::new(),
cursor: None,
extra_cursors: Vec::new(),
selection: None,
extra_selections: Vec::new(),
yank_highlight: None,
scroll_top: 0,
scroll_left: 0,
total_lines: 0,
max_col: 0,
gutter_char_width: 0,
is_active: true,
show_active_bg: false,
has_git_diff: false,
has_breakpoints: false,
diagnostic_gutter: std::collections::HashMap::new(),
code_action_lines: std::collections::HashSet::new(),
bracket_match_positions: Vec::new(),
active_indent_col: None,
tabstop: 4,
cursorline: false,
lightbulb_glyph: '\0',
};
let palette = Palette {
id: "pal".into(),
title: String::new(),
query: String::new(),
query_cursor: 0,
items: Vec::new(),
selected_idx: 0,
scroll_offset: 0,
total_count: 0,
has_focus: false,
show_query: true,
create_label: None,
preview: None,
mode: crate::primitives::palette::PaletteMode::List,
};
let mut layout = ScreenLayout::new();
layout.push(Surface::Editor {
rect: Rect::new(0.0, 0.0, 100.0, 100.0),
editor,
});
layout.push(Surface::Palette {
rect: Rect::new(0.0, 0.0, 50.0, 50.0),
palette,
});
let captured = OwnedFrame::from_layout(&layout);
assert_eq!(captured.nodes().len(), 2);
let json = serde_json::to_string(&captured).expect("OwnedFrame serializes");
let restored: OwnedFrame = serde_json::from_str(&json).expect("OwnedFrame deserializes");
let hit_map = restored.hit_map();
assert!(!hit_map.is_empty());
assert_eq!(hit_map.len(), 2);
assert_eq!(hit_map.hit_test(25.0, 25.0), FrameZone::Palette { idx: 1 });
assert_eq!(hit_map.hit_test(75.0, 75.0), FrameZone::Editor { idx: 0 });
assert_eq!(hit_map.hit_test(150.0, 150.0), FrameZone::Empty);
}
#[cfg(feature = "tui")]
#[test]
fn owned_frame_serde_round_trip_draws_through_tui_backend() {
use crate::runner::{AppLogic, Reaction};
use crate::tui::testing::TuiDriver;
use std::cell::RefCell;
let editor = Editor {
id: "ed".into(),
rect: Rect::new(0.0, 0.0, 20.0, 5.0),
lines: Vec::new(),
cursor: None,
extra_cursors: Vec::new(),
selection: None,
extra_selections: Vec::new(),
yank_highlight: None,
scroll_top: 0,
scroll_left: 0,
total_lines: 0,
max_col: 0,
gutter_char_width: 0,
is_active: true,
show_active_bg: false,
has_git_diff: false,
has_breakpoints: false,
diagnostic_gutter: std::collections::HashMap::new(),
code_action_lines: std::collections::HashSet::new(),
bracket_match_positions: Vec::new(),
active_indent_col: None,
tabstop: 4,
cursorline: false,
lightbulb_glyph: '\0',
};
let mut layout = ScreenLayout::new();
layout.push(Surface::Editor {
rect: Rect::new(0.0, 0.0, 20.0, 5.0),
editor,
});
let captured = OwnedFrame::from_layout(&layout);
let json = serde_json::to_string(&captured).expect("OwnedFrame serializes");
let restored: OwnedFrame = serde_json::from_str(&json).expect("OwnedFrame deserializes");
struct DrawRestoredFrame(OwnedFrame, RefCell<Option<FrameHitMap>>);
impl AppLogic for DrawRestoredFrame {
type AreaId = ();
fn render(&self, backend: &mut dyn Backend, _area: ()) {
let hit_map = self.0.draw(backend);
*self.1.borrow_mut() = Some(hit_map);
}
fn handle(&mut self, _event: crate::UiEvent, _backend: &mut dyn Backend) -> Reaction {
Reaction::Continue
}
}
let app = DrawRestoredFrame(restored, RefCell::new(None));
let driver = TuiDriver::new(app, 20, 5);
let hit_map = driver
.app()
.1
.borrow()
.clone()
.expect("render ran at least once during TuiDriver::new");
assert!(!hit_map.is_empty());
assert_eq!(hit_map.hit_test(1.0, 1.0), FrameZone::Editor { idx: 0 });
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TestRung {
Editor,
BottomPanel,
StatusBar,
Palette,
}
impl FrameRung for TestRung {
fn z_order() -> &'static [Self] {
&[
TestRung::Editor,
TestRung::BottomPanel,
TestRung::StatusBar,
TestRung::Palette,
]
}
}
#[test]
fn frame_rung_rank_matches_z_order_position() {
assert_eq!(TestRung::Editor.rank(), 0);
assert_eq!(TestRung::BottomPanel.rank(), 1);
assert_eq!(TestRung::StatusBar.rank(), 2);
assert_eq!(TestRung::Palette.rank(), 3);
}
#[test]
#[should_panic(expected = "missing from FrameRung::z_order()")]
fn frame_rung_rank_panics_for_unlisted_variant() {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Rogue;
impl FrameRung for Rogue {
fn z_order() -> &'static [Self] {
&[]
}
}
Rogue.rank();
}
#[test]
fn frame_presence_gates_rungs_computed_once() {
let presence = FramePresence::from_fn(|r| r != TestRung::BottomPanel);
assert!(presence.is_present(TestRung::Editor));
assert!(!presence.is_present(TestRung::BottomPanel));
assert!(presence.is_present(TestRung::StatusBar));
assert!(presence.is_present(TestRung::Palette));
assert_eq!(
presence.present_rungs().collect::<Vec<_>>(),
vec![TestRung::Editor, TestRung::StatusBar, TestRung::Palette]
);
}
#[test]
fn compose_frame_visits_only_present_rungs_in_z_order() {
let presence = FramePresence::<TestRung>::from_fn(|r| r != TestRung::Palette);
let mut painted = Vec::new();
let visited = compose_frame(&presence, |r| painted.push(r));
let expected = vec![TestRung::Editor, TestRung::BottomPanel, TestRung::StatusBar];
assert_eq!(painted, expected);
assert_eq!(visited, expected);
}
#[test]
fn check_frame_order_accepts_canonical_and_gapped_sequences() {
assert!(check_frame_order(&[
TestRung::Editor,
TestRung::BottomPanel,
TestRung::StatusBar,
TestRung::Palette,
])
.is_ok());
assert!(check_frame_order(&[TestRung::Editor, TestRung::Palette]).is_ok());
assert!(
check_frame_order(&[TestRung::Editor, TestRung::Editor, TestRung::StatusBar]).is_ok()
);
assert!(check_frame_order::<TestRung>(&[]).is_ok());
assert!(check_frame_order(&[TestRung::Palette]).is_ok());
}
#[test]
fn check_frame_order_rejects_out_of_order_visits() {
let err = check_frame_order(&[TestRung::Palette, TestRung::Editor]).unwrap_err();
assert_eq!(
err,
FrameOrderViolation {
before: TestRung::Palette,
after: TestRung::Editor,
}
);
}
#[test]
fn compose_frame_output_always_satisfies_check_frame_order() {
let presence = FramePresence::<TestRung>::from_fn(|r| r != TestRung::StatusBar);
let visited = compose_frame(&presence, |_| {});
assert!(check_frame_order(&visited).is_ok());
}
}