From b1b0999ebe282bc3620cd1b854222cb7a19986b3 Mon Sep 17 00:00:00 2001 From: wfx Date: Wed, 14 Jan 2026 17:16:25 +0100 Subject: [PATCH] chore: refactoring --- i18n/en/noctua.ftl | 20 +- .../scalable/actions/panel-left-symbolic.svg | 5 + .../scalable/actions/panel-right-symbolic.svg | 5 + src/app/message.rs | 66 +++-- src/app/mod.rs | 78 +++++- src/app/model.rs | 11 +- src/app/update.rs | 37 +-- src/app/view/canvas.rs | 7 +- src/app/view/footer.rs | 76 ++++++ src/app/view/mod.rs | 42 +--- src/app/view/panels.rs | 225 +++++------------- 11 files changed, 287 insertions(+), 285 deletions(-) create mode 100644 resources/icons/hicolor/scalable/actions/panel-left-symbolic.svg create mode 100644 resources/icons/hicolor/scalable/actions/panel-right-symbolic.svg create mode 100644 src/app/view/footer.rs diff --git a/i18n/en/noctua.ftl b/i18n/en/noctua.ftl index f4aaa52..a7796dc 100644 --- a/i18n/en/noctua.ftl +++ b/i18n/en/noctua.ftl @@ -25,8 +25,8 @@ menu-view-flip-vertical = Flip Vertically menu-view-rotate-cw = Rotate Clockwise menu-view-rotate-ccw = Rotate Counter-Clockwise -## Note messages -no_document_loaded = No document loaded. +## Placeholders / empty states +no-document = No document loaded ## Labels zoom = Zoom @@ -35,10 +35,17 @@ crop = Crop scale = Scale ## Error messages -error-failed-to-open = Failed to open “{ $path }”. +error-failed-to-open = Failed to open "{ $path }". error-unsupported-format = Unsupported file format. -# Metadata panel +## Properties panel +panel-properties = Properties +meta-dimensions = Dimensions +meta-format = Format +meta-pages = Pages +meta-current-page = Current Page + +## Metadata panel (extended) metadata = Metadata file-name = File format = Format @@ -46,7 +53,7 @@ resolution = Resolution file-size = Size color-type = Color -# EXIF data +## EXIF data exif-data = EXIF Data camera = Camera date-taken = Date @@ -56,6 +63,5 @@ iso = ISO focal-length = Focal gps = GPS -# States +## States loading-metadata = Loading... -no-document = No document diff --git a/resources/icons/hicolor/scalable/actions/panel-left-symbolic.svg b/resources/icons/hicolor/scalable/actions/panel-left-symbolic.svg new file mode 100644 index 0000000..4932d7a --- /dev/null +++ b/resources/icons/hicolor/scalable/actions/panel-left-symbolic.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/resources/icons/hicolor/scalable/actions/panel-right-symbolic.svg b/resources/icons/hicolor/scalable/actions/panel-right-symbolic.svg new file mode 100644 index 0000000..6f86e6d --- /dev/null +++ b/resources/icons/hicolor/scalable/actions/panel-right-symbolic.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/app/message.rs b/src/app/message.rs index 5a80983..9b1cb95 100644 --- a/src/app/message.rs +++ b/src/app/message.rs @@ -1,57 +1,73 @@ // SPDX-License-Identifier: GPL-3.0-or-later // src/app/message.rs // -// Top-level application messages (events, IO, and UI signals). +// All application messages (events, user actions, signals). use std::path::PathBuf; -/// Top-level application messages. -/// -/// These are produced by: -/// - UI widgets (buttons, menus, etc.) -/// - keyboard shortcuts -/// - async tasks (file loading, etc.) +use crate::app::ContextPage; + +/// Messages emitted by user actions, async I/O, or internal signals. #[derive(Debug, Clone)] pub enum AppMessage { - /// User requested to open a single file. + // === File / Navigation === + /// Open a file at the given path. OpenPath(PathBuf), - - /// Navigate to next/previous document in the current folder. + /// Navigate to the next document in folder. NextDocument, + /// Navigate to the previous document in folder. PrevDocument, - /// Refresh metadata (e.g., when panel becomes visible or document changes). - RefreshMetadata, + // === Transformations === + /// Rotate 90° clockwise. + RotateCW, + /// Rotate 90° counter-clockwise. + RotateCCW, + /// Flip horizontally (mirror). + FlipHorizontal, + /// Flip vertically. + FlipVertical, - /// Basic view / panel toggles. - ToggleLeftPanel, - ToggleRightPanel, - - /// View / zoom control. + // === Zoom === + /// Zoom in by a fixed step. ZoomIn, + /// Zoom out by a fixed step. ZoomOut, + /// Reset zoom to 100%. ZoomReset, + /// Fit document to window. ZoomFit, - /// Pan control (Ctrl + arrow keys). + // === Pan === + /// Pan image left. PanLeft, + /// Pan image right. PanRight, + /// Pan image up. PanUp, + /// Pan image down. PanDown, + /// Reset pan to center. PanReset, - /// Editing / tool modes. + // === Tool Modes === + /// Toggle crop mode. ToggleCropMode, + /// Toggle scale mode. ToggleScaleMode, - /// Document transformations. - FlipHorizontal, - FlipVertical, - RotateCW, - RotateCCW, + // === Panels (COSMIC-managed) === + /// Toggle a context drawer page. + ToggleContextPage(ContextPage), - /// Generic error reporting from lower layers. + // === Metadata === + /// Refresh metadata from the current document. + RefreshMetadata, + + // === Errors === + /// Display an error message. ShowError(String), + /// Clear the current error. ClearError, /// Fallback for unhandled or no-op cases. diff --git a/src/app/mod.rs b/src/app/mod.rs index a2b09ab..b403c76 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -10,10 +10,11 @@ pub mod update; mod view; -use cosmic::app::Core; +use cosmic::app::{context_drawer, Core}; use cosmic::iced::keyboard::{self, key::Named, Key, Modifiers}; use cosmic::iced::window; use cosmic::iced::Subscription; +use cosmic::widget::{button, icon, nav_bar}; use cosmic::{Action, Element, Task}; pub use message::AppMessage; @@ -28,10 +29,19 @@ pub enum Flags { Args(Args), } +/// Context page displayed in right drawer. +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +pub enum ContextPage { + #[default] + Properties, +} + /// Main application type. pub struct Noctua { core: Core, pub model: AppModel, + nav: nav_bar::Model, + context_page: ContextPage, } impl cosmic::Application for Noctua { @@ -49,7 +59,7 @@ impl cosmic::Application for Noctua { &mut self.core } - fn init(core: Core, flags: Self::Flags) -> (Self, Task>) { + fn init(mut core: Core, flags: Self::Flags) -> (Self, Task>) { let config = AppConfig::default(); let mut model = AppModel::new(config); @@ -59,7 +69,21 @@ impl cosmic::Application for Noctua { document::file::open_initial_path(&mut model, path); } - (Self { core, model }, Task::none()) + // Initialize empty nav bar (for folder/thumbnail navigation later). + let nav = nav_bar::Model::default(); + + // Context drawer hidden by default. + core.window.show_context = false; + + ( + Self { + core, + model, + nav, + context_page: ContextPage::default(), + }, + Task::none(), + ) } fn on_close_requested(&self, _id: window::Id) -> Option { @@ -67,6 +91,17 @@ impl cosmic::Application for Noctua { } fn update(&mut self, message: Self::Message) -> Task> { + // Handle panel toggle messages. + if let AppMessage::ToggleContextPage(page) = &message { + if self.context_page == *page { + self.core.window.show_context = !self.core.window.show_context; + } else { + self.context_page = *page; + self.core.window.show_context = true; + } + return Task::none(); + } + update::update(&mut self.model, message); Task::none() } @@ -79,6 +114,38 @@ impl cosmic::Application for Noctua { self.view() } + /// Header end items (right side of header bar). + fn header_end(&self) -> Vec> { + vec![ + // Properties panel toggle button. + button::icon(icon::from_name("document-properties-symbolic")) + .on_press(AppMessage::ToggleContextPage(ContextPage::Properties)) + .into(), + ] + } + + /// Right-side context drawer (properties panel). + fn context_drawer(&self) -> Option> { + if !self.core.window.show_context { + return None; + } + + Some(context_drawer::context_drawer( + view::panels::properties_panel(&self.model), + AppMessage::ToggleContextPage(ContextPage::Properties), + )) + } + + /// Nav bar model for left panel. + fn nav_model(&self) -> Option<&nav_bar::Model> { + Some(&self.nav) + } + + /// Footer with zoom controls and document info. + fn footer(&self) -> Option> { + Some(view::footer::view(&self.model)) + } + fn subscription(&self) -> Subscription { keyboard::on_key_press(handle_key_press) } @@ -133,6 +200,11 @@ fn handle_key_press(key: Key, modifiers: Modifiers) -> Option { // Reset pan. Key::Character("0") => Some(PanReset), + // Toggle properties panel with 'i' for info. + Key::Character(ch) if ch.eq_ignore_ascii_case("i") => { + Some(ToggleContextPage(ContextPage::Properties)) + } + _ => None, } } diff --git a/src/app/model.rs b/src/app/model.rs index a09c2d6..f475d94 100644 --- a/src/app/model.rs +++ b/src/app/model.rs @@ -5,9 +5,8 @@ use std::path::PathBuf; -use crate::app::document::DocumentContent; use crate::app::document::meta::DocumentMeta; - +use crate::app::document::DocumentContent; use crate::config::AppConfig; /// How the document is currently fitted into the window. @@ -54,7 +53,7 @@ pub struct AppModel { pub document: Option, /// Cached metadata for the current document. - /// Loaded lazily when the right panel is opened. + /// Loaded lazily when the metadata panel is opened. pub metadata: Option, /// Path of the currently opened document, if any. @@ -73,10 +72,6 @@ pub struct AppModel { pub pan_x: f32, pub pan_y: f32, - /// Panel visibility. - pub show_left_panel: bool, - pub show_right_panel: bool, - /// Current tool mode. pub tool_mode: ToolMode, @@ -97,8 +92,6 @@ impl AppModel { view_mode: ViewMode::Fit, pan_x: 0.0, pan_y: 0.0, - show_left_panel: false, - show_right_panel: false, tool_mode: ToolMode::None, error: None, } diff --git a/src/app/update.rs b/src/app/update.rs index af98bd2..9e52160 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -9,46 +9,21 @@ use super::model::{AppModel, ToolMode, ViewMode, PAN_STEP}; /// Central update function applying messages to the model. /// -/// This is the single place where application state is mutated. +/// Panel toggle messages (ToggleContextPage) are handled directly in +/// `Noctua::update()` since they affect COSMIC's Core state. pub fn update(model: &mut AppModel, msg: AppMessage) { - println!("update(): received message: {:?}", msg); - match msg { // ===== File / navigation ========================================================== AppMessage::OpenPath(path) => { document::file::open_single_file(model, &path); - // Refresh metadata if panel is visible. - if model.show_right_panel { - refresh_metadata(model); - } } AppMessage::NextDocument => { document::file::navigate_next(model); - // Refresh metadata if panel is visible. - if model.show_right_panel { - refresh_metadata(model); - } } AppMessage::PrevDocument => { document::file::navigate_prev(model); - // Refresh metadata if panel is visible. - if model.show_right_panel { - refresh_metadata(model); - } - } - - // ===== Panels ===================================================================== - AppMessage::ToggleLeftPanel => { - model.show_left_panel = !model.show_left_panel; - } - AppMessage::ToggleRightPanel => { - model.show_right_panel = !model.show_right_panel; - // Load metadata lazily when panel becomes visible. - if model.show_right_panel && model.metadata.is_none() { - refresh_metadata(model); - } } // ===== View / zoom =============================================================== @@ -80,7 +55,7 @@ pub fn update(model: &mut AppModel, msg: AppMessage) { model.reset_pan(); } - // ===== Tools ===================================================================== + // ===== Tool modes ================================================================ AppMessage::ToggleCropMode => { model.tool_mode = if model.tool_mode == ToolMode::Crop { ToolMode::None @@ -131,6 +106,11 @@ pub fn update(model: &mut AppModel, msg: AppMessage) { model.clear_error(); } + // ===== Handled elsewhere ========================================================= + AppMessage::ToggleContextPage(_) => { + // Handled in Noctua::update() directly. + } + AppMessage::NoOp => { // Intentionally do nothing. } @@ -152,7 +132,6 @@ fn zoom_out(model: &mut AppModel) { } /// Extract the current effective zoom factor from the view mode. -/// For `Fit` mode, we assume 1.0 as starting point when switching to custom zoom. fn current_zoom(model: &AppModel) -> f32 { match model.view_mode { ViewMode::Fit => 1.0, diff --git a/src/app/view/canvas.rs b/src/app/view/canvas.rs index 702f8c9..e49107c 100644 --- a/src/app/view/canvas.rs +++ b/src/app/view/canvas.rs @@ -1,8 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-or-later // src/app/view/canvas.rs // -/// Renders the center canvas area with the current document. -// +// Renders the center canvas area with the current document. + use cosmic::iced::{Alignment, Length}; use cosmic::widget::{container, image, text, Column, Row}; use cosmic::Element; @@ -55,7 +55,8 @@ pub fn view(model: &AppModel) -> Element<'_, AppMessage> { ) .into() } else { - container(text(fl!("no_document_loaded"))) + // No document loaded placeholder. + container(text(fl!("no-document"))) .center_x(Length::Fill) .center_y(Length::Fill) .width(Length::Fill) diff --git a/src/app/view/footer.rs b/src/app/view/footer.rs new file mode 100644 index 0000000..8dec49b --- /dev/null +++ b/src/app/view/footer.rs @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// src/app/view/footer.rs +// +// Footer bar with zoom controls and document info. + +use cosmic::iced::Alignment; +use cosmic::widget::{button, icon, row, text}; +use cosmic::Element; + +use crate::app::model::{AppModel, ViewMode}; +use crate::app::AppMessage; + +/// Build the footer element with zoom controls and document info. +pub fn view(model: &AppModel) -> Element<'_, AppMessage> { + // Zoom level display. + let zoom_text = match model.view_mode { + ViewMode::Fit => "Fit".to_string(), + ViewMode::ActualSize => "100%".to_string(), + ViewMode::Custom(z) => format!("{}%", (z * 100.0).round() as i32), + }; + + // Document dimensions (if available). + let doc_info = if let Some(ref doc) = model.document { + let (w, h) = doc.dimensions(); + format!("{}×{}", w, h) + } else { + String::new() + }; + + // Navigation position (e.g., "3 / 42"). + let nav_info = if !model.folder_entries.is_empty() { + let current = model.current_index.map(|i| i + 1).unwrap_or(0); + let total = model.folder_entries.len(); + format!("{} / {}", current, total) + } else { + String::new() + }; + + row() + .spacing(8) + .align_y(Alignment::Center) + .padding([4, 12]) + // Zoom out button. + .push( + button::icon(icon::from_name("zoom-out-symbolic")) + .on_press(AppMessage::ZoomOut) + .padding(4), + ) + // Zoom level display. + .push(text::body(zoom_text)) + // Zoom in button. + .push( + button::icon(icon::from_name("zoom-in-symbolic")) + .on_press(AppMessage::ZoomIn) + .padding(4), + ) + // Fit button. + .push( + button::icon(icon::from_name("zoom-fit-best-symbolic")) + .on_press(AppMessage::ZoomFit) + .padding(4), + ) + // Spacer. + .push(cosmic::widget::horizontal_space()) + // Document dimensions. + .push(text::body(doc_info)) + // Separator. + .push_maybe(if !model.folder_entries.is_empty() { + Some(text::body(" | ")) + } else { + None + }) + // Navigation position. + .push(text::body(nav_info)) + .into() +} diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index 59997cd..b2b0e59 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -1,49 +1,17 @@ // SPDX-License-Identifier: GPL-3.0-or-later // src/app/view/mod.rs // -// Root layout for the main application window. +// View module root, combining all view components. -pub mod canvas; +mod canvas; +pub mod footer; pub mod panels; use cosmic::Element; -use cosmic::iced::Length; -use cosmic::widget::{Column, Container, Row}; use crate::app::{AppMessage, AppModel}; -/// Main window layout (header, center row, footer). +/// Main application view (canvas area). pub fn view(model: &AppModel) -> Element<'_, AppMessage> { - let header = panels::header(model); - let footer = panels::footer(model); - let left_panel = panels::left_panel(model); - let right_panel = panels::right_panel(model); - let canvas = canvas::view(model); - - // Build middle row step by step to handle optional panels. - let mut middle_row = Row::new().spacing(8).height(Length::Fill); - - if let Some(left) = left_panel { - middle_row = middle_row.push(left); - } - - middle_row = middle_row.push(canvas); - - if let Some(right) = right_panel { - middle_row = middle_row.push(right); - } - - let content = Column::new() - .spacing(8) - .padding(8) - .width(Length::Fill) - .height(Length::Fill) - .push(header) - .push(middle_row) - .push(footer); - - Container::new(content) - .width(Length::Fill) - .height(Length::Fill) - .into() + canvas::view(model) } diff --git a/src/app/view/panels.rs b/src/app/view/panels.rs index cbe0068..b2f533b 100644 --- a/src/app/view/panels.rs +++ b/src/app/view/panels.rs @@ -1,197 +1,78 @@ // SPDX-License-Identifier: GPL-3.0-or-later // src/app/view/panels.rs // -// Header, footer, and side panels composing the main layout. +// Panel content for COSMIC context drawer. +use cosmic::widget::{column, row, text}; use cosmic::Element; -use cosmic::iced::{Alignment, Length}; -use cosmic::widget::{self, Column, Container, Row, Text}; -use crate::fl; -use crate::app::model::ViewMode; +use crate::app::document::DocumentContent; use crate::app::{AppMessage, AppModel}; +use crate::fl; -/// Top header bar (global actions, toggles). -pub fn header(model: &AppModel) -> Element<'_, AppMessage> { - // Left panel toggle button. - let left_toggle = widget::button::icon(widget::icon::from_name(if model.show_left_panel { - "sidebar-show-left-symbolic" - } else { - "sidebar-show-left-symbolic" - })) - .on_press(AppMessage::ToggleLeftPanel); +/// Content for the right-side properties panel (context drawer). +pub fn properties_panel(model: &AppModel) -> Element<'static, AppMessage> { + let mut content = column::with_capacity(6).spacing(12); - // Right panel toggle button. - let right_toggle = widget::button::icon(widget::icon::from_name(if model.show_right_panel { - "sidebar-show-right-symbolic" - } else { - "sidebar-show-right-symbolic" - })) - .on_press(AppMessage::ToggleRightPanel); + // Header. + let header = fl!("panel-properties"); + content = content.push(text::title4(header)); - // File name display (centered). - let file_name = model - .current_path - .as_ref() - .and_then(|p| p.file_name()) - .and_then(|n| n.to_str()) - .unwrap_or(""); + // Display document metadata if available. + if let Some(ref doc) = model.document { + match doc { + DocumentContent::Raster(raster) => { + let (w, h) = raster.dimensions(); + let format_str = raster + .path + .as_ref() + .and_then(|p| p.extension()) + .and_then(|e| e.to_str()) + .unwrap_or("unknown") + .to_uppercase(); - let title = Text::new(file_name); + let lbl_dim = fl!("meta-dimensions"); + let lbl_fmt = fl!("meta-format"); - // Spacer to push title to center and right_toggle to the right. - let left_section = Row::new() - .spacing(8) - .align_y(Alignment::Center) - .push(left_toggle); - - let center_section = Container::new(title) - .width(Length::Fill) - .align_x(Alignment::Center); - - let right_section = Row::new() - .spacing(8) - .align_y(Alignment::Center) - .push(right_toggle); - - let content = Row::new() - .spacing(8) - .align_y(Alignment::Center) - .width(Length::Fill) - .push(left_section) - .push(center_section) - .push(right_section); - - Container::new(content) - .width(Length::Fill) - .padding([4, 8]) - .into() -} - -/// Bottom footer bar (navigation & zoom). -pub fn footer(model: &AppModel) -> Element<'_, AppMessage> { - let nav = Row::new() - .spacing(4) - .align_y(Alignment::Center) - .push(widget::button::standard("<").on_press(AppMessage::PrevDocument)) - .push(widget::button::standard(">").on_press(AppMessage::NextDocument)); - - let zoom_text = match model.view_mode { - ViewMode::Fit => "Fit".to_string(), - ViewMode::ActualSize => "100%".to_string(), - ViewMode::Custom(zoom_factor) => format!("{:.0}%", zoom_factor * 100.0), - }; - - let zoom_info = Text::new(format!("Zoom: {}", zoom_text)); - - let content = Row::new() - .spacing(16) - .align_y(Alignment::Center) - .push(nav) - .push(zoom_info); - - Container::new(content) - .width(Length::Fill) - .padding([4, 8]) - .into() -} - -/// Optional left panel (tools). -pub fn left_panel(model: &AppModel) -> Option> { - if !model.show_left_panel { - return None; - } - - let tools = Column::new() - .spacing(4) - .push(Text::new(fl!("tools"))) - .push(widget::button::standard(fl!("crop")).on_press(AppMessage::ToggleCropMode)) - .push(widget::button::standard(fl!("scale")).on_press(AppMessage::ToggleScaleMode)); - - let panel = Container::new(tools) - .width(Length::Fixed(180.0)) - .height(Length::Fill) - .padding(8); - - Some(panel.into()) -} - -/// Optional right panel (metadata, info). -pub fn right_panel(model: &AppModel) -> Option> { - if !model.show_right_panel { - return None; - } - - let mut content = Column::new().spacing(8).padding(4); - - // Section header. - content = content.push(Text::new(fl!("metadata")).size(16).width(Length::Fill)); - - content = content.push(widget::divider::horizontal::default()); - - if let Some(meta) = &model.metadata { - // Basic information section. - content = content - .push(meta_row(fl!("file-name"), meta.basic.file_name.clone())) - .push(meta_row(fl!("format"), meta.basic.format.clone())) - .push(meta_row(fl!("resolution"), meta.basic.resolution_display())) - .push(meta_row(fl!("file-size"), meta.basic.file_size_display())) - .push(meta_row(fl!("color-type"), meta.basic.color_type.clone())); - - // EXIF section (if available). - if let Some(exif) = &meta.exif { - content = content - .push(widget::vertical_space().height(Length::Fixed(12.0))) - .push(Text::new(fl!("exif-data")).size(14)) - .push(widget::divider::horizontal::default()); - - if let Some(camera) = exif.camera_display() { - content = content.push(meta_row(fl!("camera"), camera)); + content = content + .push(meta_row(lbl_dim, format!("{}×{}", w, h))) + .push(meta_row(lbl_fmt, format_str)); } - if let Some(date) = &exif.date_time { - content = content.push(meta_row(fl!("date-taken"), date.clone())); + DocumentContent::Vector(vector) => { + let (w, h) = vector.dimensions(); + + let lbl_dim = fl!("meta-dimensions"); + let lbl_fmt = fl!("meta-format"); + + content = content + .push(meta_row(lbl_dim, format!("{}×{}", w, h))) + .push(meta_row(lbl_fmt, "SVG".to_string())); } - if let Some(exp) = &exif.exposure_time { - content = content.push(meta_row(fl!("exposure"), exp.clone())); - } - if let Some(aperture) = &exif.f_number { - content = content.push(meta_row(fl!("aperture"), aperture.clone())); - } - if let Some(iso) = exif.iso { - content = content.push(meta_row(fl!("iso"), iso.to_string())); - } - if let Some(focal) = &exif.focal_length { - content = content.push(meta_row(fl!("focal-length"), focal.clone())); - } - if let Some(gps) = exif.gps_display() { - content = content.push(meta_row(fl!("gps"), gps)); + DocumentContent::Portable(portable) => { + let lbl_pages = fl!("meta-pages"); + let lbl_current = fl!("meta-current-page"); + + content = content + .push(meta_row(lbl_pages, portable.page_count.to_string())) + .push(meta_row( + lbl_current, + (portable.current_page + 1).to_string(), + )); } } - } else if model.document.is_some() { - // Document exists but metadata not yet loaded. - content = content.push(Text::new(fl!("loading-metadata"))); } else { - // No document loaded. - content = content.push(Text::new(fl!("no-document"))); + let no_doc = fl!("no-document"); + content = content.push(text::body(no_doc)); } - let panel = Container::new(widget::scrollable(content).height(Length::Fill)) - .width(Length::Fixed(240.0)) - .height(Length::Fill) - .padding(8); - - Some(panel.into()) + content.into() } -/// Helper to create a label-value row for metadata display. +/// Helper to create a key-value metadata row. fn meta_row(label: String, value: String) -> Element<'static, AppMessage> { - Row::new() + row::with_capacity(2) .spacing(8) - .push( - Text::new(format!("{}:", label)) - .size(12) - .width(Length::Fixed(80.0)), - ) - .push(Text::new(value).size(12).width(Length::Fill)) + .push(text::body(format!("{}:", label))) + .push(text::body(value)) .into() }