Skip to main content

orbok_ui/
shell.rs

1//! Application shell (RFC-027): snora `AppLayout` with the sidebar
2//! navigation rail, dispatching to the page view functions.
3
4use crate::i18n::{MessageKey, tr};
5use crate::state::{AppState, Message, ViewId};
6use crate::views;
7use iced::Element;
8use snora::{AppLayout, LayoutDirection, SideBar, SideBarItem, render, widget::app_side_bar};
9
10/// The iced application wrapper around [`AppState`].
11#[derive(Default)]
12pub struct OrbokApp {
13    pub state: AppState,
14}
15
16impl OrbokApp {
17    pub fn with_state(state: AppState) -> Self {
18        Self { state }
19    }
20
21    /// iced update entry. Pure transitions only; `orbok-app` layers
22    /// backend effects on top.
23    pub fn update(&mut self, message: Message) {
24        self.state.update(&message);
25    }
26
27    /// iced view entry: sidebar + active page (RFC-027 component
28    /// mapping: AppShell → AppLayout, SidebarNav → app_side_bar).
29    pub fn view(&self) -> Element<'_, Message> {
30        let locale = self.state.locale;
31        let icon_for = |view: ViewId| -> &'static str {
32            match view {
33                ViewId::Search => "🔍",
34                ViewId::Sources => "📁",
35                ViewId::Indexing => "⏳",
36                ViewId::Storage => "💾",
37                ViewId::Models => "🧠",
38                ViewId::Settings => "⚙",
39            }
40        };
41        let tooltip_for = |view: ViewId| -> &'static str {
42            match view {
43                ViewId::Search => tr(locale, MessageKey::NavSearch),
44                ViewId::Sources => tr(locale, MessageKey::NavSources),
45                ViewId::Indexing => tr(locale, MessageKey::NavIndexing),
46                ViewId::Storage => tr(locale, MessageKey::NavStorage),
47                ViewId::Models => tr(locale, MessageKey::NavModels),
48                ViewId::Settings => tr(locale, MessageKey::NavSettings),
49            }
50        };
51        let items = ViewId::ALL
52            .iter()
53            .map(|view| SideBarItem {
54                view_id: *view,
55                icon: icon_for(*view).into(),
56                tooltip: tooltip_for(*view).into(),
57                on_press: Message::Switch(*view),
58            })
59            .collect();
60        let side_bar = app_side_bar(
61            SideBar {
62                items,
63                active: self.state.active_view,
64            },
65            LayoutDirection::Ltr,
66        );
67
68        // Startup wizard takes priority over normal navigation (design §startup).
69        if self.state.wizard.is_some() {
70            return views::wizard_view(&self.state);
71        }
72
73        let body = match self.state.active_view {
74            ViewId::Search => views::search_view(&self.state),
75            ViewId::Sources => views::sources_view(&self.state),
76            ViewId::Indexing => views::indexing_view(&self.state),
77            ViewId::Storage => views::storage_view(&self.state),
78            ViewId::Models => views::models_view(&self.state),
79            ViewId::Settings => views::settings_view(&self.state),
80        };
81
82        render(AppLayout::new(body).side_bar(side_bar))
83    }
84
85    /// Window title.
86    pub fn title(&self) -> String {
87        tr(self.state.locale, MessageKey::AppTitle).to_string()
88    }
89}