chore: refactoring
This commit is contained in:
parent
aa83e9ab1d
commit
b1b0999ebe
11 changed files with 287 additions and 285 deletions
|
|
@ -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
|
|
||||||
|
|
|
||||||
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
76
src/app/view/footer.rs
Normal 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()
|
||||||
|
}
|
||||||
|
|
@ -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()
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue