Skip to main content

orbok_ui/
state.rs

1//! Headless UI state (view models) and the message vocabulary.
2//!
3//! Everything here is plain data — testable without a display server.
4//! `orbok-app` populates these structs from backend services; views
5//! render them; `update` mutates them. No iced types appear in this
6//! module so state logic stays UI-framework-agnostic.
7
8use crate::i18n::Locale;
9use orbok_models::SearchCapability;
10
11/// Top-level pages (GUI external design §3.1 order).
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum ViewId {
14    Search,
15    Sources,
16    Indexing,
17    Storage,
18    Models,
19    Settings,
20}
21
22impl ViewId {
23    pub const ALL: &'static [ViewId] = &[
24        ViewId::Search,
25        ViewId::Sources,
26        ViewId::Indexing,
27        ViewId::Storage,
28        ViewId::Models,
29        ViewId::Settings,
30    ];
31}
32
33/// Sidebar index-health summary.
34#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
35pub struct IndexHealth {
36    pub indexed: u64,
37    pub stale: u64,
38    pub failed: u64,
39    pub queued: u64,
40}
41
42/// One source card for the Sources view.
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct SourceCard {
45    pub display_name: String,
46    pub display_path: String,
47    pub indexed: u64,
48    pub stale: u64,
49    pub failed: u64,
50    pub active: bool,
51}
52
53/// A search result ready for display — pure data, no backend types
54/// (RFC-027 boundary rule).
55#[derive(Debug, Clone, PartialEq)]
56pub struct SearchResultDisplay {
57    pub display_path: String,
58    pub title: Option<String>,
59    pub heading_path: Option<String>,
60    pub snippet: Option<String>,
61    pub keyword_rank: u32,
62    pub badges: Vec<String>,
63}
64
65/// The whole-app view model.
66#[derive(Debug, Clone)]
67pub struct AppState {
68    pub active_view: ViewId,
69    pub locale: Locale,
70    pub query: String,
71    pub last_query: Option<String>,
72    pub search_results: Vec<SearchResultDisplay>,
73    pub search_running: bool,
74    pub health: IndexHealth,
75    pub sources: Vec<SourceCard>,
76    pub capability: SearchCapability,
77    pub storage_total_bytes: u64,
78}
79
80impl Default for AppState {
81    fn default() -> Self {
82        Self {
83            active_view: ViewId::Search,
84            locale: Locale::default(),
85            query: String::new(),
86            last_query: None,
87            search_results: Vec::new(),
88            search_running: false,
89            health: IndexHealth::default(),
90            sources: Vec::new(),
91            capability: SearchCapability::KeywordOnly,
92            storage_total_bytes: 0,
93        }
94    }
95}
96
97/// UI messages.
98#[derive(Debug, Clone)]
99pub enum Message {
100    Switch(ViewId),
101    QueryChanged(String),
102    SubmitSearch,
103    SearchResultsReady(Vec<SearchResultDisplay>),
104    SearchError(String),
105    SetLocale(Locale),
106}
107
108impl AppState {
109    pub fn update(&mut self, message: &Message) {
110        match message {
111            Message::Switch(view) => self.active_view = *view,
112            Message::QueryChanged(query) => self.query = query.clone(),
113            Message::SubmitSearch => {
114                let trimmed = self.query.trim();
115                if !trimmed.is_empty() {
116                    self.last_query = Some(trimmed.to_string());
117                    self.search_running = true;
118                    self.search_results.clear();
119                }
120            }
121            Message::SearchResultsReady(results) => {
122                self.search_results = results.clone();
123                self.search_running = false;
124            }
125            Message::SearchError(_) => {
126                self.search_running = false;
127            }
128            Message::SetLocale(locale) => self.locale = *locale,
129        }
130    }
131}