Compare commits

...

4 Commits

Author SHA1 Message Date
Priec
feb22d270c flake for rust 2026-01-19 15:31:08 +01:00
Priec
d7f35690e3 working with strings to register a page, needs redesign 2026-01-19 13:10:14 +01:00
Priec
33002f89a6 default is no_std heapless 2026-01-18 12:10:43 +01:00
filipriec_vm
ad9bb78fc8 orchestrator 2026-01-12 00:59:05 +01:00
15 changed files with 975 additions and 100 deletions

50
Cargo.lock generated
View File

@@ -3,22 +3,19 @@
version = 4
[[package]]
name = "allocator-api2"
version = "0.2.21"
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "equivalent"
version = "1.0.2"
name = "hash32"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "foldhash"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606"
dependencies = [
"byteorder",
]
[[package]]
name = "hashbrown"
@@ -26,14 +23,35 @@ version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash",
"rustc-std-workspace-alloc",
]
[[package]]
name = "tui_orchestrator"
name = "heapless"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2af2455f757db2b292a9b1768c4b70186d443bcb3b316252d6b540aec1cd89ed"
dependencies = [
"hash32",
"stable_deref_trait",
]
[[package]]
name = "pages-tui"
version = "0.1.0"
dependencies = [
"hashbrown",
"heapless",
]
[[package]]
name = "rustc-std-workspace-alloc"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9d441c3b2ebf55cebf796bfdc265d67fa09db17b7bb6bd4be75c509e1e8fec3"
[[package]]
name = "stable_deref_trait"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"

View File

@@ -1,16 +1,17 @@
[package]
name = "tui_orchestrator"
name = "pages-tui"
version = "0.1.0"
edition = "2021"
license = "MIT OR Apache-2.0"
[features]
default = ["std"]
std = []
alloc = ["hashbrown"]
default = []
std = ["alloc"]
alloc = ["dep:hashbrown"]
sequences = ["alloc"]
[dependencies]
hashbrown = { version = "0.15", optional = true }
hashbrown = { version = "0.15", optional = true, default-features = false, features = ["alloc"] }
heapless = { version = "0.9.2", default-features = false }
[dev-dependencies]

75
examples/simple_usage.rs Normal file
View File

@@ -0,0 +1,75 @@
extern crate alloc;
use tui_orchestrator::prelude::*;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum Focus {
Username,
Password,
LoginButton,
}
impl FocusId for Focus {}
#[derive(Debug, Clone, PartialEq, Eq)]
enum AppEvent {
LoginAttempt {
username: alloc::string::String,
password: alloc::string::String,
},
Quit,
}
struct LoginPage {
username: alloc::string::String,
password: alloc::string::String,
}
impl Component for LoginPage {
type Focus = Focus;
type Action = ComponentAction;
type Event = AppEvent;
fn targets(&self) -> &[Self::Focus] {
&[Focus::Username, Focus::Password, Focus::LoginButton]
}
fn handle(
&mut self,
focus: &Self::Focus,
action: Self::Action,
) -> Result<Option<Self::Event>, ComponentError> {
match (focus, action) {
(Focus::LoginButton, ComponentAction::Select) => Ok(Some(AppEvent::LoginAttempt {
username: self.username.clone(),
password: self.password.clone(),
})),
(Focus::Username, ComponentAction::TypeChar(c)) => {
self.username.push(c);
Ok(None)
}
(Focus::Password, ComponentAction::TypeChar(c)) => {
self.password.push(c);
Ok(None)
}
_ => Ok(None),
}
}
}
fn main() -> Result<(), ComponentError> {
let mut orch = Orchestrator::new();
orch.bind(Key::enter(), ComponentAction::Select);
orch.bind(Key::tab(), ComponentAction::Next);
orch.bind(Key::shift_tab(), ComponentAction::Prev);
let login_page = LoginPage {
username: alloc::string::String::new(),
password: alloc::string::String::new(),
};
orch.register_page(alloc::string::String::from("login"), login_page);
Ok(())
}

49
flake.nix Normal file
View File

@@ -0,0 +1,49 @@
{
description = "Komp AC - Kompress Accounting";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
in
{
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [
mermaid-cli
# Rust toolchain
rustc
cargo
rustfmt
clippy
cargo-watch
rust-analyzer
cargo-tarpaulin
cargo-flamegraph
rust-code-analysis
# C build tools (for your linker issue)
gcc
binutils
pkg-config
# OpenSSL for crypto dependencies
openssl
openssl.dev
];
shellHook = ''
export PKG_CONFIG_PATH="${pkgs.openssl.dev}/lib/pkgconfig:$PKG_CONFIG_PATH"
export OPENSSL_DIR="${pkgs.openssl.dev}"
export OPENSSL_LIB_DIR="${pkgs.openssl.out}/lib"
export OPENSSL_INCLUDE_DIR="${pkgs.openssl.dev}/include"
echo "🦀 Rust development environment loaded"
'';
};
}
);
}

View File

@@ -2,6 +2,7 @@
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ComponentError {
EmptyTargets,
InvalidAction,
InvalidFocus,
NoComponent,
}

View File

@@ -7,22 +7,22 @@ use hashbrown::HashSet;
/// Maps keys to actions.
///
/// When `alloc` feature is enabled, uses HashMap for O(1) lookup.
/// Without `alloc`, falls back to Vec with O(n) lookup.
/// Without `alloc`, uses heapless::LinearMap with fixed capacity N.
#[derive(Debug, Clone)]
pub struct Bindings<A: Action> {
pub struct Bindings<A: Action, const N: usize = 32> {
#[cfg(feature = "alloc")]
bindings: hashbrown::HashMap<Key, A>,
#[cfg(not(feature = "alloc"))]
bindings: alloc::vec::Vec<(Key, A)>,
bindings: heapless::LinearMap<Key, A, N>,
}
impl<A: Action> Bindings<A> {
impl<A: Action, const N: usize> Bindings<A, N> {
pub fn new() -> Self {
Self {
#[cfg(feature = "alloc")]
bindings: hashbrown::HashMap::new(),
#[cfg(not(feature = "alloc"))]
bindings: alloc::vec::Vec::new(),
bindings: heapless::LinearMap::new(),
}
}
@@ -33,19 +33,14 @@ impl<A: Action> Bindings<A> {
}
#[cfg(not(feature = "alloc"))]
{
self.bindings.push((key, action));
// V heapless verzii ignorujeme chybu pri plnej kapacite,
// alebo by ste mohli vrátiť Result.
let _ = self.bindings.insert(key, action);
}
}
pub fn get(&self, key: &Key) -> Option<&A> {
#[cfg(feature = "alloc")]
{
self.bindings.get(key)
}
#[cfg(not(feature = "alloc"))]
{
self.bindings.iter().find(|(k, _)| k == key).map(|(_, a)| a)
}
self.bindings.get(key)
}
pub fn remove(&mut self, key: &Key) {
@@ -55,108 +50,85 @@ impl<A: Action> Bindings<A> {
}
#[cfg(not(feature = "alloc"))]
{
self.bindings.retain(|(k, _)| k != key);
self.bindings.remove(key);
}
}
pub fn is_empty(&self) -> bool {
#[cfg(feature = "alloc")]
{
self.bindings.is_empty()
}
#[cfg(not(feature = "alloc"))]
{
self.bindings.is_empty()
}
self.bindings.is_empty()
}
pub fn len(&self) -> usize {
#[cfg(feature = "alloc")]
{
self.bindings.len()
}
#[cfg(not(feature = "alloc"))]
{
self.bindings.len()
}
self.bindings.len()
}
#[cfg(feature = "alloc")]
pub fn keys(&self) -> alloc::vec::Vec<&Key> {
#[cfg(feature = "alloc")]
{
self.bindings.keys().collect()
}
#[cfg(not(feature = "alloc"))]
{
self.bindings.iter().map(|(k, _)| k).collect()
}
self.bindings.keys().collect()
}
#[cfg(not(feature = "alloc"))]
pub fn iter(&self) -> impl Iterator<Item = &(Key, A)> {
pub fn keys(&self) -> heapless::Vec<&Key, N> {
let mut v = heapless::Vec::new();
for k in self.bindings.keys() {
let _ = v.push(k);
}
v
}
pub fn iter(&self) -> impl Iterator<Item = (&Key, &A)> {
self.bindings.iter()
}
}
impl<A: Action> Default for Bindings<A> {
impl<A: Action, const N: usize> Default for Bindings<A, N> {
fn default() -> Self {
Self::new()
}
}
#[cfg(feature = "sequences")]
impl<A: Action + core::hash::Hash + Eq> Bindings<A> {
impl<A: Action + core::hash::Hash + Eq, const N: usize> Bindings<A, N> {
#[cfg(feature = "alloc")]
pub fn bind_sequence(&mut self, keys: alloc::vec::Vec<Key>, action: A) {
#[cfg(feature = "alloc")]
{
for key in keys {
self.bindings.insert(key, action.clone());
}
}
#[cfg(not(feature = "alloc"))]
{
for key in keys {
self.bindings.push((key, action.clone()));
}
for key in keys {
self.bindings.insert(key, action.clone());
}
}
#[cfg(not(feature = "alloc"))]
pub fn bind_sequence<const K: usize>(&mut self, keys: heapless::Vec<Key, K>, action: A) {
for key in keys {
let _ = self.bindings.insert(key, action.clone());
}
}
#[cfg(feature = "alloc")]
pub fn get_sequences(&self) -> alloc::vec::Vec<&A> {
let mut actions = alloc::vec::Vec::new();
let mut seen = HashSet::new();
#[cfg(feature = "alloc")]
{
for (_, action) in &self.bindings {
if seen.insert(action) {
actions.push(action);
}
for (_, action) in &self.bindings {
if seen.insert(action) {
actions.push(action);
}
}
#[cfg(not(feature = "alloc"))]
{
for (_, action) in &self.bindings {
if seen.insert(action) {
actions.push(action);
}
actions
}
#[cfg(not(feature = "alloc"))]
pub fn get_sequences(&self) -> heapless::Vec<&A, N> {
let mut actions = heapless::Vec::new();
// V no_std/no_alloc bez HashSetu robíme O(n^2) kontrolu unikátnosti
for (_, action) in &self.bindings {
if !actions.iter().any(|&a| a == action) {
let _ = actions.push(action);
}
}
actions
}
}
/// Returns default key bindings for common TUI patterns.
///
/// Includes:
/// - Tab/Shift+Tab for next/prev navigation
/// - Enter for select
/// - Esc for cancel
/// - Arrow keys for directional movement
/// - Home/End/PageUp/PageDown for scrolling
/// - Backspace/Delete for editing
/// - Ctrl+C as custom quit action
///
/// Use as-is or extend with your own bindings.
pub fn default_bindings() -> Bindings<ComponentAction> {
pub fn default_bindings() -> Bindings<ComponentAction, 16> {
let mut bindings = Bindings::new();
bindings.bind(Key::tab(), ComponentAction::Next);
bindings.bind(Key::shift_tab(), ComponentAction::Prev);

View File

@@ -5,9 +5,15 @@ extern crate alloc;
pub mod component;
pub mod focus;
pub mod input;
pub mod orchestrator;
pub mod prelude {
pub use crate::component::{Component, ComponentAction, ComponentError};
pub use crate::focus::{FocusError, FocusId, FocusManager, FocusQuery, Focusable};
pub use crate::input::{Action, Bindings, Key, KeyCode, KeyModifiers, MatchResult};
pub use crate::orchestrator::{
ActionResolver, CommandHandler, CommandResult, DefaultActionResolver,
DefaultCommandHandler, DefaultStateCoordinator, EventBus, EventHandler, ModeName,
ModeStack, Orchestrator, Router, RouterEvent, StateCoordinator, StateSync,
};
}

View File

@@ -0,0 +1,21 @@
// path_from_the_root: src/orchestrator/action_resolver.rs
use crate::component::Component;
pub struct ResolveContext<'a, C: Component> {
pub component: &'a C,
pub focus: &'a C::Focus,
pub action: C::Action,
}
pub trait ActionResolver<C: Component> {
fn resolve(&mut self, ctx: ResolveContext<C>) -> C::Action;
}
pub struct DefaultActionResolver;
impl<C: Component> ActionResolver<C> for DefaultActionResolver {
fn resolve(&mut self, ctx: ResolveContext<C>) -> C::Action {
ctx.action
}
}

View File

@@ -0,0 +1,47 @@
// path_from_the_root: src/orchestrator/command_handler.rs
use crate::input::Key;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CommandResult<A> {
Resolved(A),
Incomplete,
Unknown,
Exit,
}
pub trait CommandHandler<A: Clone> {
fn is_active(&self) -> bool;
fn handle(&mut self, key: Key) -> CommandResult<A>;
fn enter(&mut self);
fn exit(&mut self);
}
pub struct DefaultCommandHandler<A: Clone> {
_phantom: core::marker::PhantomData<A>,
}
impl<A: Clone> Default for DefaultCommandHandler<A> {
fn default() -> Self {
Self {
_phantom: core::marker::PhantomData,
}
}
}
impl<A: Clone> CommandHandler<A> for DefaultCommandHandler<A> {
fn is_active(&self) -> bool {
false
}
fn handle(&mut self, _key: Key) -> CommandResult<A> {
CommandResult::Exit
}
fn enter(&mut self) {}
fn exit(&mut self) {}
}

235
src/orchestrator/core.rs Normal file
View File

@@ -0,0 +1,235 @@
// path_from_the_root: src/orchestrator/core.rs
extern crate alloc;
use alloc::boxed::Box;
use alloc::string::String;
use alloc::vec::Vec;
use crate::component::{Component, ComponentError};
use crate::input::{Bindings, Key};
use crate::orchestrator::{
ActionResolver, CommandHandler, CommandResult, DefaultActionResolver, DefaultCommandHandler,
DefaultStateCoordinator, EventBus, ModeName, ModeStack, ResolveContext, Router, RouterEvent,
StateCoordinator,
};
pub struct Orchestrator<C: Component> {
router: Router<C>,
bindings: Bindings<C::Action>,
action_resolver: Box<dyn ActionResolver<C>>,
command_handler: Box<dyn CommandHandler<C::Action>>,
state_coordinator: Box<dyn StateCoordinator<C>>,
modes: ModeStack,
event_bus: EventBus<C::Event>,
running: bool,
}
impl<C: Component> Default for Orchestrator<C>
where
C::Action: Clone + 'static,
{
fn default() -> Self {
Self::new()
}
}
impl<C: Component> Orchestrator<C>
where
C::Action: Clone + 'static,
C::Event: Clone,
{
pub fn new() -> Self {
Self {
router: Router::new(),
bindings: Bindings::new(),
action_resolver: Box::new(DefaultActionResolver),
command_handler: Box::new(DefaultCommandHandler::default()),
state_coordinator: Box::new(DefaultStateCoordinator),
modes: ModeStack::new(),
event_bus: EventBus::new(),
running: true,
}
}
pub fn register_page(&mut self, id: String, page: C) {
self.router.register(id, page);
}
pub fn navigate_to(&mut self, id: String) -> Result<(), ComponentError> {
if let Some(RouterEvent::Navigated { from, to }) = self
.router
.navigate(id.clone())
.map_err(|_| ComponentError::InvalidFocus)?
{
let _ = self.state_coordinator.on_navigate(from, to);
}
Ok(())
}
pub fn back(&mut self) -> Result<(), ComponentError> {
if let Some(RouterEvent::Back { to }) = self
.router
.back()
.map_err(|_| ComponentError::InvalidFocus)?
{
let _ = self.state_coordinator.on_navigate(Some(to.clone()), to);
}
Ok(())
}
pub fn forward(&mut self) -> Result<(), ComponentError> {
if let Some(RouterEvent::Forward { to }) = self
.router
.forward()
.map_err(|_| ComponentError::InvalidFocus)?
{
let _ = self.state_coordinator.on_navigate(Some(to.clone()), to);
}
Ok(())
}
pub fn bind(&mut self, key: Key, action: C::Action) {
self.bindings.bind(key, action);
}
pub fn process_frame(&mut self, key: Key) -> Result<Vec<C::Event>, ComponentError> {
if !self.running {
return Ok(Vec::new());
}
let mut events = Vec::new();
if self.command_handler.is_active() {
match self.command_handler.handle(key) {
CommandResult::Resolved(action) => {
if let Some(event) = self.handle_action(action)? {
events.push(event);
}
}
CommandResult::Incomplete | CommandResult::Unknown => {}
CommandResult::Exit => {
self.command_handler.exit();
}
}
} else if let Some(action) = self.bindings.get(&key) {
let action = action.clone();
if let Some(_) = self.router.current_id() {
let component = self.router.current().ok_or(ComponentError::NoComponent)?;
let focus = self.router.focus_manager().current().ok_or(ComponentError::NoComponent)?;
let ctx = ResolveContext {
component,
focus,
action,
};
let resolved_action = self.action_resolver.resolve(ctx);
if let Some(event) = self.handle_action(resolved_action)? {
events.push(event);
}
}
}
for event in &events {
self.event_bus.emit(event.clone());
}
Ok(events)
}
fn handle_action(&mut self, action: C::Action) -> Result<Option<C::Event>, ComponentError> {
let focus = self.router.focus_manager().current().cloned();
if let Some(focus) = focus {
let old_focus = self.router.focus_manager().current().cloned();
let result = {
let component = self.router.current_mut().ok_or(ComponentError::NoComponent)?;
component.handle(&focus, action)?
};
let new_focus = self.router.focus_manager().current().cloned();
if old_focus != new_focus {
let _ = self.state_coordinator.on_focus_change(old_focus, new_focus);
}
Ok(result)
} else {
Ok(None)
}
}
pub fn current(&self) -> Option<&C> {
self.router.current()
}
pub fn current_mut(&mut self) -> Option<&mut C> {
self.router.current_mut()
}
pub fn focus_manager(&self) -> &crate::focus::FocusManager<C::Focus> {
self.router.focus_manager()
}
pub fn focus_manager_mut(&mut self) -> &mut crate::focus::FocusManager<C::Focus> {
self.router.focus_manager_mut()
}
pub fn modes(&self) -> &ModeStack {
&self.modes
}
pub fn modes_mut(&mut self) -> &mut ModeStack {
&mut self.modes
}
pub fn push_mode(&mut self, mode: ModeName) {
self.modes.push(mode);
}
pub fn pop_mode(&mut self) -> Option<ModeName> {
self.modes.pop()
}
pub fn event_bus(&self) -> &EventBus<C::Event> {
&self.event_bus
}
pub fn event_bus_mut(&mut self) -> &mut EventBus<C::Event> {
&mut self.event_bus
}
pub fn set_action_resolver<R: ActionResolver<C> + 'static>(&mut self, resolver: R) {
self.action_resolver = Box::new(resolver);
}
pub fn set_command_handler<H: CommandHandler<C::Action> + 'static>(
&mut self,
handler: H,
) {
self.command_handler = Box::new(handler);
}
pub fn set_state_coordinator<S: StateCoordinator<C> + 'static>(&mut self, coordinator: S) {
self.state_coordinator = Box::new(coordinator);
}
pub fn is_running(&self) -> bool {
self.running
}
pub fn stop(&mut self) {
self.running = false;
}
pub fn start(&mut self) {
self.running = true;
}
pub fn current_id(&self) -> Option<&String> {
self.router.current_id()
}
}

View File

@@ -0,0 +1,46 @@
// path_from_the_root: src/orchestrator/event_bus.rs
extern crate alloc;
use alloc::boxed::Box;
use alloc::vec::Vec;
pub trait EventHandler<E> {
fn handle(&mut self, event: E);
}
pub struct EventBus<E: Clone> {
handlers: Vec<Box<dyn EventHandler<E>>>,
}
impl<E: Clone> Default for EventBus<E> {
fn default() -> Self {
Self::new()
}
}
impl<E: Clone> EventBus<E> {
pub fn new() -> Self {
Self {
handlers: Vec::new(),
}
}
pub fn register(&mut self, handler: Box<dyn EventHandler<E>>) {
self.handlers.push(handler);
}
pub fn emit(&mut self, event: E) {
for handler in &mut self.handlers {
handler.handle(event.clone());
}
}
pub fn handler_count(&self) -> usize {
self.handlers.len()
}
pub fn is_empty(&self) -> bool {
self.handlers.is_empty()
}
}

17
src/orchestrator/mod.rs Normal file
View File

@@ -0,0 +1,17 @@
// path_from_the_root: src/orchestrator/mod.rs
pub mod action_resolver;
pub mod command_handler;
pub mod core;
pub mod event_bus;
pub mod mode;
pub mod router;
pub mod state_coordinator;
pub use action_resolver::{ActionResolver, DefaultActionResolver, ResolveContext};
pub use command_handler::{CommandHandler, CommandResult, DefaultCommandHandler};
pub use core::Orchestrator;
pub use event_bus::{EventBus, EventHandler};
pub use mode::{ModeName, ModeStack};
pub use router::{Router, RouterEvent};
pub use state_coordinator::{DefaultStateCoordinator, StateCoordinator, StateSync};

83
src/orchestrator/mode.rs Normal file
View File

@@ -0,0 +1,83 @@
// path_from_the_root: src/orchestrator/mode.rs
use alloc::collections::BTreeMap;
use alloc::string::String;
use alloc::vec::Vec;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ModeName {
General,
Navigation,
Editing,
Command,
Custom(String),
}
#[derive(Debug, Clone, Default)]
pub struct ModeStack {
modes: Vec<ModeName>,
}
impl ModeStack {
pub fn new() -> Self {
Self { modes: Vec::new() }
}
pub fn push(&mut self, mode: ModeName) {
self.modes.push(mode);
}
pub fn pop(&mut self) -> Option<ModeName> {
self.modes.pop()
}
pub fn current(&self) -> Option<&ModeName> {
self.modes.last()
}
pub fn is_empty(&self) -> bool {
self.modes.is_empty()
}
pub fn len(&self) -> usize {
self.modes.len()
}
pub fn contains(&self, mode: &ModeName) -> bool {
self.modes.contains(mode)
}
pub fn clear(&mut self) {
self.modes.clear();
}
pub fn reset(&mut self, mode: ModeName) {
self.modes.clear();
self.modes.push(mode);
}
}
#[derive(Debug, Clone, Default)]
pub struct ModeResolver {
mappings: BTreeMap<String, Vec<ModeName>>,
}
impl ModeResolver {
pub fn new() -> Self {
Self {
mappings: BTreeMap::new(),
}
}
pub fn register(&mut self, key: String, modes: Vec<ModeName>) {
self.mappings.insert(key, modes);
}
pub fn resolve(&self, key: &str) -> Option<&Vec<ModeName>> {
self.mappings.get(key)
}
pub fn is_empty(&self) -> bool {
self.mappings.is_empty()
}
}

256
src/orchestrator/router.rs Normal file
View File

@@ -0,0 +1,256 @@
// path_from_the_root: src/orchestrator/router.rs
extern crate alloc;
use alloc::string::String;
use alloc::vec::Vec;
#[cfg(feature = "alloc")]
use hashbrown::HashMap;
use crate::component::Component;
use crate::focus::{FocusError, FocusManager};
#[derive(Debug, Clone)]
pub enum RouterEvent {
Navigated { from: Option<String>, to: String },
Back { to: String },
Forward { to: String },
}
pub struct Router<C: Component> {
#[cfg(feature = "alloc")]
pages: HashMap<String, C>,
#[cfg(not(feature = "alloc"))]
pages: Vec<(String, C)>,
current: Option<String>,
history: Vec<String>,
future: Vec<String>,
focus: FocusManager<C::Focus>,
}
impl<C: Component> Default for Router<C> {
fn default() -> Self {
Self::new()
}
}
impl<C: Component> Router<C> {
pub fn new() -> Self {
Self {
#[cfg(feature = "alloc")]
pages: HashMap::new(),
#[cfg(not(feature = "alloc"))]
pages: Vec::new(),
current: None,
history: Vec::new(),
future: Vec::new(),
focus: FocusManager::new(),
}
}
pub fn register(&mut self, id: String, mut page: C) {
#[cfg(feature = "alloc")]
{
if self.current.as_ref() == Some(&id) {
let _ = page.on_enter();
let targets = page.targets();
self.focus.set_targets(targets.to_vec());
}
self.pages.insert(id, page);
}
#[cfg(not(feature = "alloc"))]
{
if self.current.as_ref() == Some(&id) {
let _ = page.on_enter();
let targets = page.targets();
self.focus.set_targets(targets.to_vec());
}
self.pages.push((id, page));
}
}
fn get_page_mut(&mut self, id: &str) -> Option<&mut C> {
#[cfg(feature = "alloc")]
{
self.pages.get_mut(id)
}
#[cfg(not(feature = "alloc"))]
{
self.pages
.iter_mut()
.find(|(page_id, _)| page_id == id)
.map(|(_, page)| page)
}
}
fn get_page(&self, id: &str) -> Option<&C> {
#[cfg(feature = "alloc")]
{
self.pages.get(id)
}
#[cfg(not(feature = "alloc"))]
{
self.pages
.iter()
.find(|(page_id, _)| page_id == id)
.map(|(_, page)| page)
}
}
pub fn navigate(&mut self, id: String) -> Result<Option<RouterEvent>, FocusError> {
let result = if let Some(current_id) = self.current.take() {
if let Some(current_page) = self.get_page_mut(&current_id) {
let _ = current_page.on_exit();
}
self.history.push(current_id.clone());
self.future.clear();
Some(RouterEvent::Navigated {
from: Some(current_id),
to: id.clone(),
})
} else {
Some(RouterEvent::Navigated {
from: None,
to: id.clone(),
})
};
let targets = if let Some(page) = self.get_page_mut(&id) {
let _ = page.on_enter();
Some(page.targets().to_vec())
} else {
None
};
if let Some(targets) = targets {
self.focus.set_targets(targets);
}
self.current = Some(id);
Ok(result)
}
pub fn back(&mut self) -> Result<Option<RouterEvent>, FocusError> {
if let Some(current) = self.current.take() {
if let Some(from) = self.history.pop() {
self.future.push(current.clone());
let to = from.clone();
if let Some(current_page) = self.get_page_mut(&current) {
let _ = current_page.on_exit();
}
self.current = Some(to.clone());
let targets = if let Some(page) = self.get_page_mut(&to) {
let _ = page.on_enter();
Some(page.targets().to_vec())
} else {
None
};
if let Some(targets) = targets {
self.focus.set_targets(targets);
}
Ok(Some(RouterEvent::Back { to }))
} else {
self.current = Some(current);
Ok(None)
}
} else {
Ok(None)
}
}
pub fn forward(&mut self) -> Result<Option<RouterEvent>, FocusError> {
if let Some(current) = self.current.take() {
if let Some(from) = self.future.pop() {
self.history.push(current.clone());
let to = from.clone();
if let Some(current_page) = self.get_page_mut(&current) {
let _ = current_page.on_exit();
}
self.current = Some(to.clone());
let targets = if let Some(page) = self.get_page_mut(&to) {
let _ = page.on_enter();
Some(page.targets().to_vec())
} else {
None
};
if let Some(targets) = targets {
self.focus.set_targets(targets);
}
Ok(Some(RouterEvent::Forward { to }))
} else {
self.current = Some(current);
Ok(None)
}
} else {
Ok(None)
}
}
pub fn current(&self) -> Option<&C> {
self.current.as_ref().and_then(|id| self.get_page(id))
}
pub fn current_mut(&mut self) -> Option<&mut C> {
if let Some(id) = self.current.clone() {
self.get_page_mut(&id)
} else {
None
}
}
pub fn current_id(&self) -> Option<&String> {
self.current.as_ref()
}
pub fn focus_manager(&self) -> &FocusManager<C::Focus> {
&self.focus
}
pub fn focus_manager_mut(&mut self) -> &mut FocusManager<C::Focus> {
&mut self.focus
}
pub fn history(&self) -> &[String] {
&self.history
}
pub fn future(&self) -> &[String] {
&self.future
}
pub fn page_count(&self) -> usize {
#[cfg(feature = "alloc")]
{
self.pages.len()
}
#[cfg(not(feature = "alloc"))]
{
self.pages.len()
}
}
pub fn has_page(&self, id: &str) -> bool {
#[cfg(feature = "alloc")]
{
self.pages.contains_key(id)
}
#[cfg(not(feature = "alloc"))]
{
self.pages.iter().any(|(page_id, _)| page_id == id)
}
}
}

View File

@@ -0,0 +1,48 @@
// path_from_the_root: src/orchestrator/state_coordinator.rs
use alloc::string::String;
use alloc::vec::Vec;
use crate::component::Component;
pub enum StateSync {
Synced,
Conflict { details: String },
}
pub trait StateCoordinator<C: Component> {
fn on_navigate(&mut self, from: Option<String>, to: String) -> Result<StateSync, String>;
fn on_focus_change(
&mut self,
old: Option<C::Focus>,
new: Option<C::Focus>,
) -> Result<StateSync, String>;
fn on_mode_change(&mut self, _old: Vec<String>, _new: Vec<String>)
-> Result<StateSync, String>;
}
pub struct DefaultStateCoordinator;
impl<C: Component> StateCoordinator<C> for DefaultStateCoordinator {
fn on_navigate(&mut self, _from: Option<String>, _to: String) -> Result<StateSync, String> {
Ok(StateSync::Synced)
}
fn on_focus_change(
&mut self,
_old: Option<C::Focus>,
_new: Option<C::Focus>,
) -> Result<StateSync, String> {
Ok(StateSync::Synced)
}
fn on_mode_change(
&mut self,
_old: Vec<String>,
_new: Vec<String>,
) -> Result<StateSync, String> {
Ok(StateSync::Synced)
}
}