Files
mnote/rust/crates/mnote-web/src/tree_shell/renderer_input.rs
T

266 lines
10 KiB
Rust
Raw Normal View History

2026-04-26 19:35:52 +08:00
use super::filetree_selection::{FileTreeSelectionReducerContract, FileTreeSelectionState};
use super::focus_state::PageFocusKeyboardReducerContract;
use super::picker_state::PickerStateReducerContract;
use serde::Serialize;
use std::collections::BTreeSet;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum TreeShellRendererMode {
Page,
FileTree,
Picker,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TreeShellRendererInput {
pub mode: TreeShellRendererMode,
pub projection_item_ids: Vec<String>,
pub expanded_ids: BTreeSet<String>,
pub focused_id: Option<String>,
pub page_focus_keyboard_reducer: Option<PageFocusKeyboardReducerContract>,
pub filetree_selection: FileTreeSelectionState,
pub filetree_selection_reducer: Option<FileTreeSelectionReducerContract>,
pub active_picker_item: Option<String>,
pub excluded_picker_ids: BTreeSet<String>,
pub picker_state_reducer: Option<PickerStateReducerContract>,
pub command_dispatcher: TreeShellCommandDispatcher,
pub runtime_artifact: TreeShellRuntimeArtifactBoundary,
2026-04-26 19:35:52 +08:00
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TreeShellCommandDispatcher {
pub channel: String,
pub command_names: BTreeSet<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TreeShellRuntimeArtifactBoundary {
pub contract_name: &'static str,
pub input_fields: BTreeSet<&'static str>,
pub output_channels: BTreeSet<&'static str>,
pub event_kinds: BTreeSet<&'static str>,
}
impl Default for TreeShellRuntimeArtifactBoundary {
fn default() -> Self {
Self {
contract_name: "rust_tree_shell_runtime_artifact_v1",
input_fields: BTreeSet::from([
"rendererInput",
"projectionItems",
"expandedIds",
"selectedRowIds",
"activePickerItem",
"focusedId",
]),
output_channels: BTreeSet::from(["domPatch", "intentEvent", "commandDispatchEvent"]),
event_kinds: BTreeSet::from([
"focus",
"keyboard",
"expandCollapse",
"selection",
"contextMenu",
"dragDrop",
"pick",
]),
}
}
}
2026-04-26 19:35:52 +08:00
impl TreeShellRendererInput {
pub fn page(input: PageTreeRendererInput) -> Self {
Self {
mode: TreeShellRendererMode::Page,
projection_item_ids: input.projection_item_ids,
expanded_ids: input.expanded_ids,
focused_id: input.focused_id,
page_focus_keyboard_reducer: Some(PageFocusKeyboardReducerContract::default()),
filetree_selection: FileTreeSelectionState::default(),
filetree_selection_reducer: None,
active_picker_item: None,
excluded_picker_ids: BTreeSet::new(),
picker_state_reducer: None,
command_dispatcher: input.command_dispatcher,
runtime_artifact: TreeShellRuntimeArtifactBoundary::default(),
2026-04-26 19:35:52 +08:00
}
}
pub fn filetree(input: FileTreeRendererInput) -> Self {
Self {
mode: TreeShellRendererMode::FileTree,
projection_item_ids: input.projection_item_ids,
expanded_ids: input.expanded_ids,
focused_id: input.filetree_selection.focused_row_id.clone(),
page_focus_keyboard_reducer: None,
filetree_selection: input.filetree_selection,
filetree_selection_reducer: Some(FileTreeSelectionReducerContract::default()),
active_picker_item: None,
excluded_picker_ids: BTreeSet::new(),
picker_state_reducer: None,
command_dispatcher: input.command_dispatcher,
runtime_artifact: TreeShellRuntimeArtifactBoundary::default(),
2026-04-26 19:35:52 +08:00
}
}
pub fn picker(input: PickerRendererInput) -> Self {
Self {
mode: TreeShellRendererMode::Picker,
projection_item_ids: input.projection_item_ids,
expanded_ids: input.expanded_ids,
focused_id: input.active_picker_item.clone(),
page_focus_keyboard_reducer: None,
filetree_selection: FileTreeSelectionState::default(),
filetree_selection_reducer: None,
active_picker_item: input.active_picker_item,
excluded_picker_ids: input.excluded_picker_ids,
picker_state_reducer: Some(PickerStateReducerContract::default()),
command_dispatcher: input.command_dispatcher,
runtime_artifact: TreeShellRuntimeArtifactBoundary::default(),
2026-04-26 19:35:52 +08:00
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PageTreeRendererInput {
pub projection_item_ids: Vec<String>,
pub expanded_ids: BTreeSet<String>,
pub focused_id: Option<String>,
pub command_dispatcher: TreeShellCommandDispatcher,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FileTreeRendererInput {
pub projection_item_ids: Vec<String>,
pub expanded_ids: BTreeSet<String>,
pub filetree_selection: FileTreeSelectionState,
pub command_dispatcher: TreeShellCommandDispatcher,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PickerRendererInput {
pub projection_item_ids: Vec<String>,
pub expanded_ids: BTreeSet<String>,
pub active_picker_item: Option<String>,
pub excluded_picker_ids: BTreeSet<String>,
pub command_dispatcher: TreeShellCommandDispatcher,
}
#[cfg(test)]
mod tests {
use super::{
FileTreeRendererInput, PageTreeRendererInput, PickerRendererInput,
TreeShellCommandDispatcher, TreeShellRendererInput, TreeShellRendererMode,
2026-04-26 19:35:52 +08:00
};
use crate::tree_shell::filetree_selection::FileTreeSelectionState;
use std::collections::BTreeSet;
fn set(values: &[&str]) -> BTreeSet<String> {
values.iter().map(|value| (*value).to_string()).collect()
}
fn dispatcher() -> TreeShellCommandDispatcher {
TreeShellCommandDispatcher {
channel: "mnote.tree.shell".into(),
command_names: set(&["tree.node.create", "tree.resource.move"]),
}
}
#[test]
fn tree_shell_renderer_input_contract_covers_page_filetree_and_picker_state() {
let page = TreeShellRendererInput::page(PageTreeRendererInput {
projection_item_ids: vec!["doc:root".into()],
expanded_ids: set(&["doc:root"]),
focused_id: Some("doc:root".into()),
command_dispatcher: dispatcher(),
});
assert_eq!(page.mode, TreeShellRendererMode::Page);
assert_eq!(page.focused_id.as_deref(), Some("doc:root"));
assert_eq!(page.command_dispatcher.channel, "mnote.tree.shell");
assert_eq!(
page.page_focus_keyboard_reducer
.as_ref()
.map(|contract| contract.contract_name),
Some("rust_page_focus_keyboard_reducer_v1")
);
let mut selection = FileTreeSelectionState::from_selected(&["asset:a".into()]);
selection.focused_row_id = Some("asset:a".into());
let filetree = TreeShellRendererInput::filetree(FileTreeRendererInput {
projection_item_ids: vec!["doc:root".into(), "asset:a".into()],
expanded_ids: set(&["doc:root"]),
filetree_selection: selection,
command_dispatcher: dispatcher(),
});
assert_eq!(filetree.mode, TreeShellRendererMode::FileTree);
assert_eq!(filetree.focused_id.as_deref(), Some("asset:a"));
assert!(filetree
.filetree_selection
.selected_row_ids
.contains("asset:a"));
2026-04-26 19:35:52 +08:00
assert!(filetree.page_focus_keyboard_reducer.is_none());
assert_eq!(
filetree
.filetree_selection_reducer
.as_ref()
.map(|contract| contract.contract_name),
Some("rust_filetree_selection_reducer_v1")
);
let picker = TreeShellRendererInput::picker(PickerRendererInput {
projection_item_ids: vec!["doc:root".into(), "doc:child".into()],
expanded_ids: set(&["doc:root"]),
active_picker_item: Some("doc:child".into()),
excluded_picker_ids: set(&["doc:archived"]),
command_dispatcher: dispatcher(),
});
assert_eq!(picker.mode, TreeShellRendererMode::Picker);
assert_eq!(picker.focused_id.as_deref(), Some("doc:child"));
assert!(picker.excluded_picker_ids.contains("doc:archived"));
assert!(picker.page_focus_keyboard_reducer.is_none());
assert!(picker.filetree_selection_reducer.is_none());
assert_eq!(
picker
.picker_state_reducer
.as_ref()
.map(|contract| contract.contract_name),
Some("rust_picker_state_reducer_v1")
);
}
#[test]
fn tree_shell_renderer_input_exposes_runtime_artifact_boundary() {
let page = TreeShellRendererInput::page(PageTreeRendererInput {
projection_item_ids: vec!["doc:root".into()],
expanded_ids: set(&["doc:root"]),
focused_id: Some("doc:root".into()),
command_dispatcher: dispatcher(),
});
let artifact = page.runtime_artifact;
assert_eq!(
artifact.contract_name,
"rust_tree_shell_runtime_artifact_v1"
);
assert!(artifact.input_fields.contains("rendererInput"));
assert!(artifact.input_fields.contains("projectionItems"));
assert!(artifact.input_fields.contains("expandedIds"));
assert!(artifact.input_fields.contains("selectedRowIds"));
assert!(artifact.input_fields.contains("activePickerItem"));
assert!(artifact.input_fields.contains("focusedId"));
assert!(artifact.output_channels.contains("domPatch"));
assert!(artifact.output_channels.contains("intentEvent"));
assert!(artifact.output_channels.contains("commandDispatchEvent"));
assert!(artifact.event_kinds.contains("focus"));
assert!(artifact.event_kinds.contains("keyboard"));
assert!(artifact.event_kinds.contains("expandCollapse"));
assert!(artifact.event_kinds.contains("selection"));
assert!(artifact.event_kinds.contains("contextMenu"));
assert!(artifact.event_kinds.contains("dragDrop"));
assert!(artifact.event_kinds.contains("pick"));
}
2026-04-26 19:35:52 +08:00
}