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