chore: refactoring

This commit is contained in:
wfx 2026-01-14 17:16:25 +01:00
parent aa83e9ab1d
commit b1b0999ebe
11 changed files with 287 additions and 285 deletions

View file

@ -25,8 +25,8 @@ menu-view-flip-vertical = Flip Vertically
menu-view-rotate-cw = Rotate Clockwise menu-view-rotate-cw = Rotate Clockwise
menu-view-rotate-ccw = Rotate Counter-Clockwise menu-view-rotate-ccw = Rotate Counter-Clockwise
## Note messages ## Placeholders / empty states
no_document_loaded = No document loaded. no-document = No document loaded
## Labels ## Labels
zoom = Zoom zoom = Zoom
@ -35,10 +35,17 @@ crop = Crop
scale = Scale scale = Scale
## Error messages ## Error messages
error-failed-to-open = Failed to open “{ $path }”. error-failed-to-open = Failed to open "{ $path }".
error-unsupported-format = Unsupported file format. 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 metadata = Metadata
file-name = File file-name = File
format = Format format = Format
@ -46,7 +53,7 @@ resolution = Resolution
file-size = Size file-size = Size
color-type = Color color-type = Color
# EXIF data ## EXIF data
exif-data = EXIF Data exif-data = EXIF Data
camera = Camera camera = Camera
date-taken = Date date-taken = Date
@ -56,6 +63,5 @@ iso = ISO
focal-length = Focal focal-length = Focal
gps = GPS gps = GPS
# States ## States
loading-metadata = Loading... loading-metadata = Loading...
no-document = No document

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<rect x="1" y="2" width="14" height="12" rx="1" ry="1" fill="none" stroke="currentColor" stroke-width="1.5"/>
<line x1="5" y1="2" x2="5" y2="14" stroke="currentColor" stroke-width="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 322 B

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<rect x="1" y="2" width="14" height="12" rx="1" ry="1" fill="none" stroke="currentColor" stroke-width="1.5"/>
<line x1="11" y1="2" x2="11" y2="14" stroke="currentColor" stroke-width="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 324 B

View file

@ -1,57 +1,73 @@
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// src/app/message.rs // src/app/message.rs
// //
// Top-level application messages (events, IO, and UI signals). // All application messages (events, user actions, signals).
use std::path::PathBuf; use std::path::PathBuf;
/// Top-level application messages. use crate::app::ContextPage;
///
/// These are produced by: /// Messages emitted by user actions, async I/O, or internal signals.
/// - UI widgets (buttons, menus, etc.)
/// - keyboard shortcuts
/// - async tasks (file loading, etc.)
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum AppMessage { pub enum AppMessage {
/// User requested to open a single file. // === File / Navigation ===
/// Open a file at the given path.
OpenPath(PathBuf), OpenPath(PathBuf),
/// Navigate to the next document in folder.
/// Navigate to next/previous document in the current folder.
NextDocument, NextDocument,
/// Navigate to the previous document in folder.
PrevDocument, PrevDocument,
/// Refresh metadata (e.g., when panel becomes visible or document changes). // === Transformations ===
RefreshMetadata, /// Rotate 90° clockwise.
RotateCW,
/// Rotate 90° counter-clockwise.
RotateCCW,
/// Flip horizontally (mirror).
FlipHorizontal,
/// Flip vertically.
FlipVertical,
/// Basic view / panel toggles. // === Zoom ===
ToggleLeftPanel, /// Zoom in by a fixed step.
ToggleRightPanel,
/// View / zoom control.
ZoomIn, ZoomIn,
/// Zoom out by a fixed step.
ZoomOut, ZoomOut,
/// Reset zoom to 100%.
ZoomReset, ZoomReset,
/// Fit document to window.
ZoomFit, ZoomFit,
/// Pan control (Ctrl + arrow keys). // === Pan ===
/// Pan image left.
PanLeft, PanLeft,
/// Pan image right.
PanRight, PanRight,
/// Pan image up.
PanUp, PanUp,
/// Pan image down.
PanDown, PanDown,
/// Reset pan to center.
PanReset, PanReset,
/// Editing / tool modes. // === Tool Modes ===
/// Toggle crop mode.
ToggleCropMode, ToggleCropMode,
/// Toggle scale mode.
ToggleScaleMode, ToggleScaleMode,
/// Document transformations. // === Panels (COSMIC-managed) ===
FlipHorizontal, /// Toggle a context drawer page.
FlipVertical, ToggleContextPage(ContextPage),
RotateCW,
RotateCCW,
/// Generic error reporting from lower layers. // === Metadata ===
/// Refresh metadata from the current document.
RefreshMetadata,
// === Errors ===
/// Display an error message.
ShowError(String), ShowError(String),
/// Clear the current error.
ClearError, ClearError,
/// Fallback for unhandled or no-op cases. /// Fallback for unhandled or no-op cases.

View file

@ -10,10 +10,11 @@ pub mod update;
mod view; mod view;
use cosmic::app::Core; use cosmic::app::{context_drawer, Core};
use cosmic::iced::keyboard::{self, key::Named, Key, Modifiers}; use cosmic::iced::keyboard::{self, key::Named, Key, Modifiers};
use cosmic::iced::window; use cosmic::iced::window;
use cosmic::iced::Subscription; use cosmic::iced::Subscription;
use cosmic::widget::{button, icon, nav_bar};
use cosmic::{Action, Element, Task}; use cosmic::{Action, Element, Task};
pub use message::AppMessage; pub use message::AppMessage;
@ -28,10 +29,19 @@ pub enum Flags {
Args(Args), Args(Args),
} }
/// Context page displayed in right drawer.
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
pub enum ContextPage {
#[default]
Properties,
}
/// Main application type. /// Main application type.
pub struct Noctua { pub struct Noctua {
core: Core, core: Core,
pub model: AppModel, pub model: AppModel,
nav: nav_bar::Model,
context_page: ContextPage,
} }
impl cosmic::Application for Noctua { impl cosmic::Application for Noctua {
@ -49,7 +59,7 @@ impl cosmic::Application for Noctua {
&mut self.core &mut self.core
} }
fn init(core: Core, flags: Self::Flags) -> (Self, Task<Action<Self::Message>>) { fn init(mut core: Core, flags: Self::Flags) -> (Self, Task<Action<Self::Message>>) {
let config = AppConfig::default(); let config = AppConfig::default();
let mut model = AppModel::new(config); let mut model = AppModel::new(config);
@ -59,7 +69,21 @@ impl cosmic::Application for Noctua {
document::file::open_initial_path(&mut model, path); 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<Self::Message> { fn on_close_requested(&self, _id: window::Id) -> Option<Self::Message> {
@ -67,6 +91,17 @@ impl cosmic::Application for Noctua {
} }
fn update(&mut self, message: Self::Message) -> Task<Action<Self::Message>> { fn update(&mut self, message: Self::Message) -> Task<Action<Self::Message>> {
// 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); update::update(&mut self.model, message);
Task::none() Task::none()
} }
@ -79,6 +114,38 @@ impl cosmic::Application for Noctua {
self.view() self.view()
} }
/// Header end items (right side of header bar).
fn header_end(&self) -> Vec<Element<Self::Message>> {
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<context_drawer::ContextDrawer<Self::Message>> {
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<Element<Self::Message>> {
Some(view::footer::view(&self.model))
}
fn subscription(&self) -> Subscription<Self::Message> { fn subscription(&self) -> Subscription<Self::Message> {
keyboard::on_key_press(handle_key_press) keyboard::on_key_press(handle_key_press)
} }
@ -133,6 +200,11 @@ fn handle_key_press(key: Key, modifiers: Modifiers) -> Option<AppMessage> {
// Reset pan. // Reset pan.
Key::Character("0") => Some(PanReset), 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, _ => None,
} }
} }

View file

@ -5,9 +5,8 @@
use std::path::PathBuf; use std::path::PathBuf;
use crate::app::document::DocumentContent;
use crate::app::document::meta::DocumentMeta; use crate::app::document::meta::DocumentMeta;
use crate::app::document::DocumentContent;
use crate::config::AppConfig; use crate::config::AppConfig;
/// How the document is currently fitted into the window. /// How the document is currently fitted into the window.
@ -54,7 +53,7 @@ pub struct AppModel {
pub document: Option<DocumentContent>, pub document: Option<DocumentContent>,
/// Cached metadata for the current document. /// 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<DocumentMeta>, pub metadata: Option<DocumentMeta>,
/// Path of the currently opened document, if any. /// Path of the currently opened document, if any.
@ -73,10 +72,6 @@ pub struct AppModel {
pub pan_x: f32, pub pan_x: f32,
pub pan_y: f32, pub pan_y: f32,
/// Panel visibility.
pub show_left_panel: bool,
pub show_right_panel: bool,
/// Current tool mode. /// Current tool mode.
pub tool_mode: ToolMode, pub tool_mode: ToolMode,
@ -97,8 +92,6 @@ impl AppModel {
view_mode: ViewMode::Fit, view_mode: ViewMode::Fit,
pan_x: 0.0, pan_x: 0.0,
pan_y: 0.0, pan_y: 0.0,
show_left_panel: false,
show_right_panel: false,
tool_mode: ToolMode::None, tool_mode: ToolMode::None,
error: None, error: None,
} }

View file

@ -9,46 +9,21 @@ use super::model::{AppModel, ToolMode, ViewMode, PAN_STEP};
/// Central update function applying messages to the model. /// 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) { pub fn update(model: &mut AppModel, msg: AppMessage) {
println!("update(): received message: {:?}", msg);
match msg { match msg {
// ===== File / navigation ========================================================== // ===== File / navigation ==========================================================
AppMessage::OpenPath(path) => { AppMessage::OpenPath(path) => {
document::file::open_single_file(model, &path); document::file::open_single_file(model, &path);
// Refresh metadata if panel is visible.
if model.show_right_panel {
refresh_metadata(model);
}
} }
AppMessage::NextDocument => { AppMessage::NextDocument => {
document::file::navigate_next(model); document::file::navigate_next(model);
// Refresh metadata if panel is visible.
if model.show_right_panel {
refresh_metadata(model);
}
} }
AppMessage::PrevDocument => { AppMessage::PrevDocument => {
document::file::navigate_prev(model); 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 =============================================================== // ===== View / zoom ===============================================================
@ -80,7 +55,7 @@ pub fn update(model: &mut AppModel, msg: AppMessage) {
model.reset_pan(); model.reset_pan();
} }
// ===== Tools ===================================================================== // ===== Tool modes ================================================================
AppMessage::ToggleCropMode => { AppMessage::ToggleCropMode => {
model.tool_mode = if model.tool_mode == ToolMode::Crop { model.tool_mode = if model.tool_mode == ToolMode::Crop {
ToolMode::None ToolMode::None
@ -131,6 +106,11 @@ pub fn update(model: &mut AppModel, msg: AppMessage) {
model.clear_error(); model.clear_error();
} }
// ===== Handled elsewhere =========================================================
AppMessage::ToggleContextPage(_) => {
// Handled in Noctua::update() directly.
}
AppMessage::NoOp => { AppMessage::NoOp => {
// Intentionally do nothing. // Intentionally do nothing.
} }
@ -152,7 +132,6 @@ fn zoom_out(model: &mut AppModel) {
} }
/// Extract the current effective zoom factor from the view mode. /// 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 { fn current_zoom(model: &AppModel) -> f32 {
match model.view_mode { match model.view_mode {
ViewMode::Fit => 1.0, ViewMode::Fit => 1.0,

View file

@ -1,8 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// src/app/view/canvas.rs // 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::iced::{Alignment, Length};
use cosmic::widget::{container, image, text, Column, Row}; use cosmic::widget::{container, image, text, Column, Row};
use cosmic::Element; use cosmic::Element;
@ -55,7 +55,8 @@ pub fn view(model: &AppModel) -> Element<'_, AppMessage> {
) )
.into() .into()
} else { } else {
container(text(fl!("no_document_loaded"))) // No document loaded placeholder.
container(text(fl!("no-document")))
.center_x(Length::Fill) .center_x(Length::Fill)
.center_y(Length::Fill) .center_y(Length::Fill)
.width(Length::Fill) .width(Length::Fill)

76
src/app/view/footer.rs Normal file
View file

@ -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()
}

View file

@ -1,49 +1,17 @@
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// src/app/view/mod.rs // 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; pub mod panels;
use cosmic::Element; use cosmic::Element;
use cosmic::iced::Length;
use cosmic::widget::{Column, Container, Row};
use crate::app::{AppMessage, AppModel}; use crate::app::{AppMessage, AppModel};
/// Main window layout (header, center row, footer). /// Main application view (canvas area).
pub fn view(model: &AppModel) -> Element<'_, AppMessage> { pub fn view(model: &AppModel) -> Element<'_, AppMessage> {
let header = panels::header(model); canvas::view(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()
} }

View file

@ -1,197 +1,78 @@
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// src/app/view/panels.rs // 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::Element;
use cosmic::iced::{Alignment, Length};
use cosmic::widget::{self, Column, Container, Row, Text};
use crate::fl; use crate::app::document::DocumentContent;
use crate::app::model::ViewMode;
use crate::app::{AppMessage, AppModel}; use crate::app::{AppMessage, AppModel};
use crate::fl;
/// Top header bar (global actions, toggles). /// Content for the right-side properties panel (context drawer).
pub fn header(model: &AppModel) -> Element<'_, AppMessage> { pub fn properties_panel(model: &AppModel) -> Element<'static, AppMessage> {
// Left panel toggle button. let mut content = column::with_capacity(6).spacing(12);
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);
// Right panel toggle button. // Header.
let right_toggle = widget::button::icon(widget::icon::from_name(if model.show_right_panel { let header = fl!("panel-properties");
"sidebar-show-right-symbolic" content = content.push(text::title4(header));
} else {
"sidebar-show-right-symbolic"
}))
.on_press(AppMessage::ToggleRightPanel);
// File name display (centered). // Display document metadata if available.
let file_name = model if let Some(ref doc) = model.document {
.current_path match doc {
.as_ref() DocumentContent::Raster(raster) => {
.and_then(|p| p.file_name()) let (w, h) = raster.dimensions();
.and_then(|n| n.to_str()) let format_str = raster
.unwrap_or(""); .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. content = content
let left_section = Row::new() .push(meta_row(lbl_dim, format!("{}×{}", w, h)))
.spacing(8) .push(meta_row(lbl_fmt, format_str));
.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<Element<'_, AppMessage>> {
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<Element<'_, AppMessage>> {
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));
} }
if let Some(date) = &exif.date_time { DocumentContent::Vector(vector) => {
content = content.push(meta_row(fl!("date-taken"), date.clone())); 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 { DocumentContent::Portable(portable) => {
content = content.push(meta_row(fl!("exposure"), exp.clone())); let lbl_pages = fl!("meta-pages");
} let lbl_current = fl!("meta-current-page");
if let Some(aperture) = &exif.f_number {
content = content.push(meta_row(fl!("aperture"), aperture.clone())); content = content
} .push(meta_row(lbl_pages, portable.page_count.to_string()))
if let Some(iso) = exif.iso { .push(meta_row(
content = content.push(meta_row(fl!("iso"), iso.to_string())); lbl_current,
} (portable.current_page + 1).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));
} }
} }
} else if model.document.is_some() {
// Document exists but metadata not yet loaded.
content = content.push(Text::new(fl!("loading-metadata")));
} else { } else {
// No document loaded. let no_doc = fl!("no-document");
content = content.push(Text::new(fl!("no-document"))); content = content.push(text::body(no_doc));
} }
let panel = Container::new(widget::scrollable(content).height(Length::Fill)) content.into()
.width(Length::Fixed(240.0))
.height(Length::Fill)
.padding(8);
Some(panel.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> { fn meta_row(label: String, value: String) -> Element<'static, AppMessage> {
Row::new() row::with_capacity(2)
.spacing(8) .spacing(8)
.push( .push(text::body(format!("{}:", label)))
Text::new(format!("{}:", label)) .push(text::body(value))
.size(12)
.width(Length::Fixed(80.0)),
)
.push(Text::new(value).size(12).width(Length::Fill))
.into() .into()
} }