chore: initial commit

This commit is contained in:
wfx 2026-01-07 20:22:49 +01:00
commit ab93f649bd
31 changed files with 9918 additions and 0 deletions

65
src/app/view/canvas.rs Normal file
View file

@ -0,0 +1,65 @@
// SPDX-License-Identifier: MPL-2.0
// src/app/view/canvas.rs
//
// Center canvas for displaying the current document.
use cosmic::iced::{Alignment, Length};
use cosmic::widget::{container, image, text, Column, Row};
use cosmic::Element;
use crate::fl;
use crate::app::model::ViewMode;
use crate::app::{AppMessage, AppModel};
/// Render the center canvas area with the current document.
pub fn view(model: &AppModel) -> Element<'_, AppMessage> {
if let Some(doc) = &model.document {
let handle = doc.handle();
let img_widget = match &model.view_mode {
ViewMode::Fit => {
// Fit mode: image scales to fill container while preserving aspect ratio.
image::Image::new(handle)
.width(Length::Fill)
.height(Length::Fill)
}
ViewMode::ActualSize => {
// 1:1 pixel size.
let (native_w, native_h) = doc.dimensions();
image::Image::new(handle)
.width(Length::Fixed(native_w as f32))
.height(Length::Fixed(native_h as f32))
}
ViewMode::Custom(_) => {
// Custom zoom factor applied to native size.
let (native_w, native_h) = doc.dimensions();
let scaled_w = (native_w as f32 * model.zoom).round();
let scaled_h = (native_h as f32 * model.zoom).round();
image::Image::new(handle)
.width(Length::Fixed(scaled_w))
.height(Length::Fixed(scaled_h))
}
};
// Center the image both horizontally and vertically.
Column::new()
.width(Length::Fill)
.height(Length::Fill)
.align_x(Alignment::Center)
.push(
Row::new()
.push(img_widget)
.width(Length::Fill)
.height(Length::Fill)
.align_y(Alignment::Center),
)
.into()
} else {
container(text(fl!("no_document_loaded")))
.center_x(Length::Fill)
.center_y(Length::Fill)
.width(Length::Fill)
.height(Length::Fill)
.into()
}
}

49
src/app/view/mod.rs Normal file
View file

@ -0,0 +1,49 @@
// SPDX-License-Identifier: MPL-2.0
// src/app/view/mod.rs
//
// Root layout for the main application window.
pub mod canvas;
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).
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()
}

89
src/app/view/panels.rs Normal file
View file

@ -0,0 +1,89 @@
// SPDX-License-Identifier: MPL-2.0
// src/app/view/panels.rs
//
// Header, footer, and side panels composing the main layout.
use cosmic::Element;
use cosmic::iced::{Alignment, Length};
use cosmic::widget::{self, Column, Container, Row, Text};
use crate::fl;
use crate::app::{AppMessage, AppModel};
/// Top header bar (global actions, toggles).
pub fn header(_model: &AppModel) -> Element<'_, AppMessage> {
let content = Row::new().spacing(8).align_y(Alignment::Center);
//.push(Text::new(fl!("noctua-app-name")).size(18));
// In a real implementation, add more buttons/actions here.
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_info = Text::new(format!("Zoom: {:.0}%", model.zoom * 100.0));
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("Tools"))
.push(widget::button::standard("Crop").on_press(AppMessage::ToggleCropMode))
.push(widget::button::standard("Scale").on_press(AppMessage::ToggleScaleMode));
// Later: color pickers, marker tools, text tool, etc.
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 meta = Column::new()
.spacing(4)
.push(Text::new("Metadata"))
.push(Text::new(format!(
"Current index: {:?}",
model.current_index
)));
// Later: real EXIF / tags from model.metadata_cache
let panel = Container::new(meta)
.width(Length::Fixed(220.0))
.height(Length::Fill)
.padding(8);
Some(panel.into())
}