From 1861f357a8b94e647e7179619f902f76b1b04081 Mon Sep 17 00:00:00 2001 From: Chris Cochrun Date: Tue, 26 Aug 2025 15:25:04 -0500 Subject: [PATCH] well crap --- Cargo.lock | 98 +++++- Cargo.toml | 2 + src/core/model.rs | 1 - src/core/service_items.rs | 44 +-- src/core/songs.rs | 1 - src/core/videos.rs | 1 - src/main.rs | 550 +++++++++++++++------------------- src/ui/library.rs | 181 +++++------ src/ui/presenter.rs | 116 +++---- src/ui/slide_editor.rs | 51 ++-- src/ui/song_editor.rs | 69 +++-- src/ui/text_svg.rs | 6 +- src/ui/widgets/icon/handle.rs | 115 +++++++ src/ui/widgets/icon/mod.rs | 187 ++++++++++++ src/ui/widgets/icon/named.rs | 165 ++++++++++ src/ui/widgets/mod.rs | 1 + 16 files changed, 1026 insertions(+), 562 deletions(-) create mode 100644 src/ui/widgets/icon/handle.rs create mode 100644 src/ui/widgets/icon/mod.rs create mode 100644 src/ui/widgets/icon/named.rs diff --git a/Cargo.lock b/Cargo.lock index 4e2f6cd..91bd7bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1496,6 +1496,41 @@ dependencies = [ "winreg 0.52.0", ] +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.106", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.106", +] + [[package]] name = "dasp_sample" version = "0.11.0" @@ -1545,6 +1580,18 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive_setters" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae5c625eda104c228c06ecaf988d1c60e542176bd7a490e60eeda3493244c0c9" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "detect-desktop-environment" version = "0.2.0" @@ -1969,6 +2016,12 @@ dependencies = [ "spin", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foldhash" version = "0.1.5" @@ -2066,6 +2119,20 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "freedesktop-icons" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95f87364ea709292a3b3f74014ce3ee78412c89807eea75a358c8e029b000994" +dependencies = [ + "dirs 5.0.1", + "ini_core", + "once_cell", + "thiserror 1.0.69", + "tracing", + "xdg", +] + [[package]] name = "futures" version = "0.3.31" @@ -2440,7 +2507,7 @@ dependencies = [ "log", "presser", "thiserror 1.0.69", - "windows 0.54.0", + "windows 0.58.0", ] [[package]] @@ -3239,6 +3306,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.0.3" @@ -3333,6 +3406,15 @@ dependencies = [ "hashbrown 0.15.5", ] +[[package]] +name = "ini_core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a467a31a9f439b5262fa99c17084537bff57f24703d5a09a2b5c9657ec73a61" +dependencies = [ + "cfg-if", +] + [[package]] name = "instant" version = "0.1.13" @@ -3730,7 +3812,9 @@ dependencies = [ "clap", "colors-transform", "crisp", + "derive_setters", "dirs 5.0.1", + "freedesktop-icons", "gstreamer", "gstreamer-app", "iced 0.14.0-dev", @@ -7385,7 +7469,7 @@ dependencies = [ "js-sys", "log", "naga 0.19.2", - "parking_lot 0.11.2", + "parking_lot 0.12.4", "profiling", "raw-window-handle 0.6.2", "smallvec", @@ -7442,7 +7526,7 @@ dependencies = [ "log", "naga 0.19.2", "once_cell", - "parking_lot 0.11.2", + "parking_lot 0.12.4", "profiling", "raw-window-handle 0.6.2", "rustc-hash 1.1.0", @@ -7542,7 +7626,7 @@ dependencies = [ "ndk-sys 0.5.0+25.2.9519653", "objc", "once_cell", - "parking_lot 0.11.2", + "parking_lot 0.12.4", "profiling", "range-alloc", "raw-window-handle 0.6.2", @@ -8342,6 +8426,12 @@ version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" +[[package]] +name = "xdg" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" + [[package]] name = "xdg-home" version = "1.3.0" diff --git a/Cargo.toml b/Cargo.toml index 7418e9b..37a0430 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,8 @@ rayon = "1.11.0" # wgpu = "26.0.1" # mupdf = "0.5.0" rfd = { version = "0.12.1", features = ["xdg-portal"], default-features = false } +derive_setters = "0.1.8" +freedesktop-icons = "0.4.0" [dependencies.iced] git = "https://github.com/iced-rs/iced" diff --git a/src/core/model.rs b/src/core/model.rs index 3520dc0..8452cc1 100644 --- a/src/core/model.rs +++ b/src/core/model.rs @@ -1,6 +1,5 @@ use std::mem::replace; -use iced::iced::Executor; use miette::{miette, Result}; use sqlx::{Connection, SqliteConnection}; diff --git a/src/core/service_items.rs b/src/core/service_items.rs index 2d885d0..15673d0 100644 --- a/src/core/service_items.rs +++ b/src/core/service_items.rs @@ -4,7 +4,7 @@ use std::ops::Deref; use std::sync::Arc; use crisp::types::{Keyword, Symbol, Value}; -use iced::iced::clipboard::mime::{AllowedMimeTypes, AsMimeTypes}; +// use cosmic::iced::clipboard::mime::{AllowedMimeTypes, AsMimeTypes}; use miette::Result; use tracing::{debug, error}; @@ -56,29 +56,29 @@ impl TryFrom<(Vec, String)> for ServiceItem { } } -impl AllowedMimeTypes for ServiceItem { - fn allowed() -> Cow<'static, [String]> { - Cow::from(vec!["application/service-item".to_string()]) - } -} +// impl AllowedMimeTypes for ServiceItem { +// fn allowed() -> Cow<'static, [String]> { +// Cow::from(vec!["application/service-item".to_string()]) +// } +// } -impl AsMimeTypes for ServiceItem { - fn available(&self) -> Cow<'static, [String]> { - debug!(?self); - Cow::from(vec!["application/service-item".to_string()]) - } +// impl AsMimeTypes for ServiceItem { +// fn available(&self) -> Cow<'static, [String]> { +// debug!(?self); +// Cow::from(vec!["application/service-item".to_string()]) +// } - fn as_bytes( - &self, - mime_type: &str, - ) -> Option> { - debug!(?self); - debug!(mime_type); - let val = Value::from(self); - let val = String::from(val); - Some(Cow::from(val.into_bytes())) - } -} +// fn as_bytes( +// &self, +// mime_type: &str, +// ) -> Option> { +// debug!(?self); +// debug!(mime_type); +// let val = Value::from(self); +// let val = String::from(val); +// Some(Cow::from(val.into_bytes())) +// } +// } impl From<&ServiceItem> for Value { fn from(value: &ServiceItem) -> Self { diff --git a/src/core/songs.rs b/src/core/songs.rs index 988a51b..270bc00 100644 --- a/src/core/songs.rs +++ b/src/core/songs.rs @@ -1,7 +1,6 @@ use std::{collections::HashMap, option::Option, path::PathBuf}; use crisp::types::{Keyword, Symbol, Value}; -use iced::iced::Executor; use miette::{miette, IntoDiagnostic, Result}; use serde::{Deserialize, Serialize}; use sqlx::{ diff --git a/src/core/videos.rs b/src/core/videos.rs index 0a761a5..879c14a 100644 --- a/src/core/videos.rs +++ b/src/core/videos.rs @@ -8,7 +8,6 @@ use super::{ slide::Slide, }; use crisp::types::{Keyword, Symbol, Value}; -use iced::iced::Executor; use miette::{IntoDiagnostic, Result}; use serde::{Deserialize, Serialize}; use sqlx::{ diff --git a/src/main.rs b/src/main.rs index e2d63fe..6e85e37 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,8 +7,8 @@ use iced::keyboard::{Key, Modifiers}; use iced::theme::{self, Palette}; use iced::widget::tooltip::Position as TPosition; use iced::widget::{ - button, horizontal_space, slider, text, tooltip, vertical_space, - Space, + button, horizontal_space, mouse_area, slider, text, text_input, + tooltip, vertical_space, Space, }; use iced::widget::{column, row}; use iced::window::{Mode, Position}; @@ -31,6 +31,8 @@ use ui::presenter::{self, Presenter}; use ui::song_editor::{self, SongEditor}; use ui::EditorMode; +use crate::ui::widgets::icon; + pub mod core; pub mod lisp; pub mod ui; @@ -62,26 +64,24 @@ fn main() -> Result<()> { .with_timer(timer) .init(); - let args = Cli::parse(); + // let settings; + // if args.ui { + // debug!("main view"); + // settings = iced::daemon::Settings::default() + // .debug(false) + // .is_daemon(true) + // .transparent(true); + // } else { + // debug!("window view"); + // settings = Settings::default() + // .debug(false) + // .no_main_window(true) + // .is_daemon(true); + // } - let settings; - if args.ui { - debug!("main view"); - settings = Settings::default() - .debug(false) - .is_daemon(true) - .transparent(true); - } else { - debug!("window view"); - settings = Settings::default() - .debug(false) - .no_main_window(true) - .is_daemon(true); - } - - iced::daemon(move || App::init(args), App::update, App::view) - .settings(settings) - .subscription(App::subsrciption) + iced::daemon(App::init, App::update, App::view) + .settings(Settings::default()) + .subscription(App::subscription) .theme(App::theme) .title(App::title) .run() @@ -128,10 +128,10 @@ enum Message { ChangeServiceItem(usize), AddServiceItem(usize, ServiceItem), AddServiceItemDrop(usize), - AppendServiceItem(ServiceItem), + AppendServiceItem(Option), } -#[derive(Debug)] +#[derive(Debug, Clone)] struct Window { title: String, scale_input: String, @@ -145,31 +145,66 @@ impl Default for Window { title: Default::default(), scale_input: Default::default(), current_scale: Default::default(), - theme: App::theme(), + theme: Theme::custom( + "Snazzy", + Palette { + background: color!(0x282a36), + text: color!(0xe2e4e5), + primary: color!(0x57c7ff), + success: color!(0x5af78e), + warning: color!(0xff9f43), + danger: color!(0xff5c57), + }, + ), } } } -const HEADER_SPACE: u16 = 6; +const HEADER_SPACE: f32 = 6.0; impl App { const APP_ID: &'static str = "lumina"; - fn init(input: Cli) -> (Self, Task) { + fn title(&self, id: window::Id) -> String { + self.windows + .get(&id) + .map(|window| window.title.clone()) + .unwrap_or(String::from("Lumina")) + } + + fn init() -> (Self, Task) { debug!("init"); + let args = Cli::parse(); let mut batch = vec![]; let mut windows = BTreeMap::new(); - if input.ui { + if args.ui { let settings = window::Settings { ..Default::default() }; let (id, open) = window::open(settings); - batch.push(open); + batch + .push(open.map(|id| Message::WindowOpened(id, None))); + let window = Window { + title: "Lumina".into(), + scale_input: "".into(), + current_scale: 1.0, + theme: Theme::custom( + "Snazzy", + Palette { + background: color!(0x282a36), + text: color!(0xe2e4e5), + primary: color!(0x57c7ff), + success: color!(0x5af78e), + warning: color!(0xff9f43), + danger: color!(0xff5c57), + }, + ), + }; - windows.insert(id, Window::default()); + windows.insert(id, window); } - let items = match read_to_string(input.file) { + let items = match read_to_string(args.file) { Ok(lisp) => { let mut slide_vector = vec![]; let lisp = crisp::reader::read(&lisp); @@ -210,7 +245,7 @@ impl App { file: PathBuf::default(), windows, presentation_open: false, - cli_mode: !input.ui, + cli_mode: !args.ui, library: None, library_open: true, library_width: 60.0, @@ -221,9 +256,9 @@ impl App { library_dragged_item: None, }; - if input.ui { + if args.ui { debug!("main view"); - batch.push(app.update_title()) + // batch.push(app.update_title()) } else { debug!("window view"); batch.push(app.show_window()) @@ -236,9 +271,9 @@ impl App { } fn nav_bar(&self) -> Option> { - if !self.core().nav_bar_active() { - return None; - } + // if !self.core().nav_bar_active() { + // return None; + // } // let nav_model = self.nav_model()?; @@ -271,89 +306,52 @@ impl App { // .width(Length::Shrink) // .height(Length::Shrink); - let list = self - .service - .iter() - .enumerate() - .map(|(index, item)| { - let button = button(item.title.clone() - .leading_icon({ - match item.kind { - core::kinds::ServiceItemKind::Song(_) => { - icon::from_name("folder-music-symbolic") - }, - core::kinds::ServiceItemKind::Video(_) => { - icon::from_name("folder-videos-symbolic") - }, - core::kinds::ServiceItemKind::Image(_) => { - icon::from_name("folder-pictures-symbolic") - }, - core::kinds::ServiceItemKind::Presentation(_) => { - icon::from_name("x-office-presentation-symbolic") - }, - core::kinds::ServiceItemKind::Content(_) => { - icon::from_name("x-office-presentation-symbolic") - }, - } - })) - .class(iced::theme::style::Button::HeaderBar) - .padding(5) - .width(Length::Fill) - .on_press(iced::Action::App(Message::ChangeServiceItem(index))); - let tooltip = tooltip(button, - text::body(item.kind.to_string()), - TPosition::Right); - dnd_destination(tooltip, vec!["application/service-item".into()]) - .data_received_for::( move |item| { - if let Some(item) = item { - iced::Action::App(Message::AddServiceItem(index, item)) - } else { - iced::Action::None - } - }).on_drop(move |x, y| { - debug!(x, y); - iced::Action::App(Message::AddServiceItemDrop(index)) - }).on_finish(move |mime, data, action, x, y| { - debug!(mime, ?data, ?action, x, y); - let Ok(item) = ServiceItem::try_from((data, mime)) else { - return iced::Action::None; - }; - debug!(?item); - iced::Action::App(Message::AddServiceItem(index, item)) - }) + let list = + self.service.iter().enumerate().map(|(index, item)| { + let icon = match item.kind { + core::kinds::ServiceItemKind::Song(_) => { + icon::from_name("folder-music-symbolic") + } + core::kinds::ServiceItemKind::Video(_) => { + icon::from_name("folder-videos-symbolic") + } + core::kinds::ServiceItemKind::Image(_) => { + icon::from_name("folder-pictures-symbolic") + } + core::kinds::ServiceItemKind::Presentation(_) => { + icon::from_name( + "x-office-presentation-symbolic", + ) + } + core::kinds::ServiceItemKind::Content(_) => { + icon::from_name( + "x-office-presentation-symbolic", + ) + } + }; + let button = + button(row![text(item.title.clone()), icon]) + .padding(5) + .width(Length::Fill) + .on_press(Message::ChangeServiceItem(index)); + let tooltip = tooltip( + button, + text(item.kind.to_string()), + TPosition::Right, + ); + mouse_area(tooltip) + .on_release(Message::AddServiceItemDrop(index)) .into() }); let end_index = self.service.len(); let column = column![ - text::heading("Service List").center().width(280), + text("Service List").center().width(280), column(list).spacing(10), - dnd_destination( - vertical_space(), - vec!["application/service-item".into()] - ) - .data_received_for::(|item| { - if let Some(item) = item { - iced::Action::App(Message::AppendServiceItem( - item, - )) - } else { - iced::Action::None - } - }) - .on_finish( - move |mime, data, action, x, y| { - debug!(mime, ?data, ?action, x, y); - let Ok(item) = - ServiceItem::try_from((data, mime)) - else { - return iced::Action::None; - }; - debug!(?item); - iced::Action::App(Message::AddServiceItem( - end_index, item, - )) - } + mouse_area(vertical_space(),).on_release( + Message::AppendServiceItem( + self.library_dragged_item.clone() + ) ) ] .padding(10) @@ -361,52 +359,51 @@ impl App { let padding = Padding::new(0.0).top(20); let mut container = Container::new(column) // .height(Length::Fill) - .style(nav_bar_style) + // .style(nav_bar_style) .padding(padding); - if !self.core().is_condensed() { - container = container.max_width(280); - } + // if !self.core().is_condensed() { + // container = container.max_width(280); + // } Some(container.into()) } - fn header_start(&self) -> Vec> { + fn header_start(&self) -> Vec> { vec![] } - fn header_center(&self) -> Vec> { - vec![search_input("Search...", "") + fn header_center(&self) -> Vec> { + vec![text_input("Search...", "") .on_input(|_| Message::None) - .on_submit(|_| Message::None) - .on_focus(Message::SearchFocus) + .on_submit(Message::None) .width(1200) .into()] } - fn header_end(&self) -> Vec> { + fn header_end(&self) -> Vec> { // let editor_toggle = toggler(self.editor_mode.is_some()) // .label("Editor") // .spacing(10) // .width(Length::Shrink) // .on_toggle(Message::EditorToggle); - let presenter_window = self.windows.get(1); - let text = if self.presentation_open { - text::body("End Presentation") + let presenter_window = self.windows.len() > 1; + let presentation_button_text = if self.presentation_open { + text("End Presentation") } else { - text::body("Present") + text("Present") }; let row = row![ tooltip( - button::custom( + button( row!( Container::new( icon::from_name("document-edit-symbolic") .scale(3) ) .center_y(Length::Fill), - text::body(if self.editor_mode.is_some() { + text(if self.editor_mode.is_some() { "Present Mode" } else { "Edit Mode" @@ -414,7 +411,6 @@ impl App { ) .spacing(5), ) - .class(iced::theme::style::Button::HeaderBar) .on_press(Message::EditorToggle( self.editor_mode.is_none(), )), @@ -422,7 +418,7 @@ impl App { TPosition::Bottom, ), tooltip( - button::custom( + button( row!( Container::new( icon::from_name( @@ -435,16 +431,16 @@ impl App { .scale(3) ) .center_y(Length::Fill), - text + presentation_button_text ) .spacing(5), ) - .class(iced::theme::style::Button::HeaderBar) .on_press({ if self.presentation_open { - Message::CloseWindow( - presenter_window.copied(), - ) + // Message::CloseWindow( + // presenter_window.copied(), + // ) + Message::None } else { Message::OpenWindow } @@ -453,14 +449,14 @@ impl App { TPosition::Bottom, ), tooltip( - button::custom( + button( row!( Container::new( icon::from_name("view-list-symbolic") .scale(3) ) .center_y(Length::Fill), - text::body(if self.library_open { + text(if self.library_open { "Close Library" } else { "Open Library" @@ -468,7 +464,6 @@ impl App { ) .spacing(5), ) - .class(iced::theme::style::Button::HeaderBar) .on_press(Message::LibraryToggle), "Open Library", TPosition::Bottom, @@ -479,25 +474,23 @@ impl App { vec![row] } - fn footer(&self) -> Option> { + fn footer(&self) -> Option> { let total_items_text = format!("Total Service Items: {}", self.service.len()); let total_slides_text = format!("Total Slides: {}", self.presenter.total_slides); - let row = row![ - text::body(total_items_text), - text::body(total_slides_text) - ] - .spacing(10); + let row = + row![text(total_items_text), text(total_slides_text)] + .spacing(10); Some( Container::new(row) .align_right(Length::Fill) - .padding([5, 0, 0, 0]) + .padding([5, 0]) .into(), ) } - fn subscription(&self) -> Subscription { + fn subscription(&self) -> Subscription { event::listen_with(|event, _, id| { // debug!(?event); match event { @@ -530,31 +523,12 @@ impl App { } } iced::Event::Touch(_touch) => None, - iced::Event::A11y(_id, _action_request) => None, - iced::Event::Dnd(_dnd_event) => None, - iced::Event::PlatformSpecific(_platform_specific) => { - None - } + iced::Event::InputMethod(event) => todo!(), } }) } - fn context_drawer( - &self, - ) -> Option> - { - ContextDrawer { - title: Some("Context".into()), - header_actions: vec![], - header: Some("hi".into()), - content: "Sup".into(), - footer: Some("foot".into()), - on_close: Message::None, - }; - None - } - - fn dialog(&self) -> Option> { + fn dialog(&self) -> Option> { if self.searching { Some(text("hello").into()) } else { @@ -571,9 +545,7 @@ impl App { // debug!(?message); match self.song_editor.update(message) { song_editor::Action::Task(task) => { - task.map(|m| { - iced::Action::App(Message::SongEditor(m)) - }) + task.map(|m| Message::SongEditor(m)) } song_editor::Action::UpdateSong(song) => { if let Some(library) = &mut self.library { @@ -595,10 +567,9 @@ impl App { video.set_muted(false); } match self.presenter.update(message) { - presenter::Action::Task(task) => task.map(|m| { - // debug!("Should run future"); - iced::Action::App(Message::Present(m)) - }), + presenter::Action::Task(task) => { + task.map(|m| Message::Present(m)) + } presenter::Action::None => Task::none(), presenter::Action::NextSlide => { let slide_index = self.current_item.1; @@ -626,9 +597,7 @@ impl App { match action { presenter::Action::Task(task) => { tasks.push(task.map(|m| { - iced::Action::App( - Message::Present(m), - ) + Message::Present(m) })) } _ => todo!(), @@ -648,13 +617,7 @@ impl App { presenter::Action::Task( task, ) => tasks.push(task.map( - |m| { - iced::Action::App( - Message::Present( - m, - ), - ) - }, + |m| Message::Present(m), )), _ => todo!(), } @@ -684,9 +647,7 @@ impl App { match action { presenter::Action::Task(task) => { tasks.push(task.map(|m| { - iced::Action::App( - Message::Present(m), - ) + Message::Present(m) })) } _ => todo!(), @@ -721,13 +682,7 @@ impl App { presenter::Action::Task( task, ) => tasks.push(task.map( - |m| { - iced::Action::App( - Message::Present( - m, - ), - ) - }, + |m| Message::Present(m), )), _ => todo!(), } @@ -749,9 +704,7 @@ impl App { } library::Action::Task(task) => { return task.map(|message| { - iced::Action::App(Message::Library( - message, - )) + Message::Library(message) }); } library::Action::None => return Task::none(), @@ -796,27 +749,7 @@ impl App { self.file = file; Task::none() } - Message::OpenWindow => { - let count = self.windows.len() + 1; - - let (id, spawn_window) = - window::open(window::Settings { - position: Position::Centered, - exit_on_close_request: count % 2 == 0, - decorations: false, - ..Default::default() - }); - - self.windows.push(id); - _ = self.set_window_title( - format!("window_{}", count), - id, - ); - - spawn_window.map(|id| { - iced::Action::App(Message::WindowOpened(id, None)) - }) - } + Message::OpenWindow => self.show_window(), Message::CloseWindow(id) => { if let Some(id) = id { window::close(id) @@ -826,27 +759,21 @@ impl App { } Message::WindowOpened(id, _) => { debug!(?id, "Window opened"); - if self.cli_mode - || id > self.core.main_window_id().expect("Iced core seems to be missing a main window, was this started in cli mode?") - { - self.presentation_open = true; - if let Some(video) = &mut self.presenter.video { - video.set_muted(false); - } - window::change_mode(id, Mode::Fullscreen) - } else { - Task::none() - } + let main_id = + self.windows.first_key_value().unwrap().0; + if self.cli_mode || &id > main_id { + self.presentation_open = true; + if let Some(video) = &mut self.presenter.video { + video.set_muted(false); + } + window::set_mode(id, Mode::Fullscreen) + } else { + Task::none() + } } Message::WindowClosed(id) => { warn!("Closing window: {id}"); - let Some(window) = - self.windows.iter().position(|w| *w == id) - else { - error!("Nothing matches this window id: {id}"); - return Task::none(); - }; - self.windows.remove(window); + self.windows.remove(&id); // This closes the app if using the cli example if self.windows.is_empty() { self.update(Message::Quit) @@ -862,9 +789,8 @@ impl App { self.library_open = !self.library_open; Task::none() } - Message::Quit => iced::iced::exit(), - Message::DndEnter(entity, data) => { - debug!(?entity); + Message::Quit => iced::exit(), + Message::DndEnter(data) => { debug!(?data); Task::none() } @@ -884,10 +810,6 @@ impl App { // } let item = library.get_song(item).unwrap(); let item = ServiceItem::from(item); - self.nav_model - .insert() - .text(item.title.clone()) - .data(item); } Task::none() } @@ -896,7 +818,7 @@ impl App { Task::none() } Message::None => Task::none(), - Message::DndLeave(entity) => { + Message::DndLeave() => { // debug!(?entity); Task::none() } @@ -940,14 +862,24 @@ impl App { Task::none() } Message::AppendServiceItem(item) => { - self.service.push(item); + if let Some(item) = item { + self.service.push(item); + } Task::none() } } } // Main window view - fn view(&self) -> Element { + fn view(&self, window: window::Id) -> Element { + if window == *self.windows.first_key_value().unwrap().0 { + self.main_view() + } else { + self.view_presenter() + } + } + + fn main_view(&self) -> Element { let icon_left = icon::from_name("arrow-left"); let icon_right = icon::from_name("arrow-right"); @@ -956,25 +888,30 @@ impl App { |video| video.duration().as_secs_f32(), ); - let video_button_icon = - if let Some(video) = &self.presenter.video { - let (icon_name, tooltip) = if video.paused() { - ("media-play", "Play") - } else { - ("media-pause", "Pause") - }; - button::icon(icon::from_name(icon_name)) - .tooltip(tooltip) - .on_press(Message::Present( - presenter::Message::StartVideo, - )) + let video_button_icon = if let Some(video) = + &self.presenter.video + { + let (icon_name, tt) = if video.paused() { + ("media-play", "Play") } else { - button::icon(icon::from_name("media-play")) - .tooltip("Play") - .on_press(Message::Present( - presenter::Message::StartVideo, - )) + ("media-pause", "Pause") }; + tooltip( + button(icon::from_name(icon_name)).on_press( + Message::Present(presenter::Message::StartVideo), + ), + tt, + TPosition::FollowCursor, + ) + } else { + tooltip( + button(icon::from_name("media-play")).on_press( + Message::Present(presenter::Message::StartVideo), + ), + "Play", + TPosition::FollowCursor, + ) + }; let slide_preview = column![ Space::with_height(Length::Fill), @@ -999,7 +936,7 @@ impl App { .step(0.1) ) .center_x(Length::Fill) - .padding([7, 0, 0, 0]) + .padding([7, 0]) ] .padding(5) } else { @@ -1016,7 +953,7 @@ impl App { } else { Space::new(0, 0).into() }) - .style(nav_bar_style) + // .style(nav_bar_style) .center(Length::FillPortion(2)) } else { Container::new(horizontal_space().width(0)) @@ -1026,31 +963,25 @@ impl App { self.song_editor.view().map(Message::SongEditor); let row = row![ - Container::new( - button::icon(icon_left) - .icon_size(128) - .tooltip("Previous Slide") - .width(128) - .on_press(Message::Present( - presenter::Message::PrevSlide - )) - .class(theme::style::Button::Transparent) - ) + Container::new(tooltip( + button(icon_left.size(128)).width(128).on_press( + Message::Present(presenter::Message::PrevSlide) + ), + "Previous Slide", + TPosition::FollowCursor + )) .center_y(Length::Fill) .align_right(Length::FillPortion(1)), Container::new(slide_preview) .center_y(Length::Fill) .width(Length::FillPortion(3)), - Container::new( - button::icon(icon_right) - .icon_size(128) - .tooltip("Next Slide") - .width(128) - .on_press(Message::Present( - presenter::Message::NextSlide - )) - .class(theme::style::Button::Transparent) - ) + Container::new(tooltip( + button(icon_right.size(128)).width(128).on_press( + Message::Present(presenter::Message::NextSlide) + ), + "Next Slide", + TPosition::FollowCursor + )) .center_y(Length::Fill) .align_left(Length::FillPortion(1)), library @@ -1077,49 +1008,38 @@ impl App { } // View for presentation - fn view_window(&self, _id: window::Id) -> Element { + fn view_presenter(&self) -> Element { self.presenter.view().map(Message::Present) } -} -impl App -where - Self: iced::Application, -{ - fn active_page_title(&self) -> &str { - let Some(label) = - self.nav_model.text(self.nav_model.active()) - else { - return "Lumina"; - }; - label - } - - fn update_title(&mut self) -> Task { - let header_title = self.active_page_title().to_owned(); - let window_title = format!("{header_title} — Lumina"); - self.core.main_window_id().map_or_else(Task::none, |id| { - self.set_window_title(window_title, id) - }) - } + // fn update_title(&mut self) -> Task { + // let window_title = "Lumina"; + // self.windows.first_key_value().unwrap().1.title = + // window_title.to_string(); + // Task::none() + // } fn show_window(&mut self) -> Task { let (id, spawn_window) = window::open(window::Settings { position: Position::Centered, exit_on_close_request: true, decorations: false, + fullscreen: true, ..Default::default() }); - self.windows.push(id); - _ = self.set_window_title("Lumina Presenter".to_owned(), id); - spawn_window.map(|id| { - iced::Action::App(Message::WindowOpened(id, None)) - }) + let window = Window { + title: "Presentation".into(), + scale_input: "".into(), + current_scale: 1.0, + theme: self.theme(id), + }; + self.windows.insert(id, window); + spawn_window.map(|id| Message::WindowOpened(id, None)) } fn add_library(&mut self) -> Task { Task::perform(async move { Library::new().await }, |x| { - iced::Action::App(Message::AddLibrary(x)) + Message::AddLibrary(x) }) } @@ -1191,7 +1111,7 @@ where } } - fn theme() -> Theme { + fn theme(&self, _window: window::Id) -> Theme { Theme::custom( "Snazzy", Palette { diff --git a/src/ui/library.rs b/src/ui/library.rs index 2d172bf..12e092b 100644 --- a/src/ui/library.rs +++ b/src/ui/library.rs @@ -4,24 +4,27 @@ use iced::{ futures::FutureExt, theme, widget::{ - button, container, horizontal_space, mouse_area, responsive, - row, scrollable, text, text_input, Container, Space, + button, column, container, horizontal_space, mouse_area, + responsive, row, scrollable, text, text_input, Container, + Space, }, - widget::{column, row as rowm, text as textm}, Background, Border, Color, Element, Length, Task, }; use miette::{IntoDiagnostic, Result}; use sqlx::{pool::PoolConnection, Sqlite, SqlitePool}; use tracing::{debug, error, warn}; -use crate::core::{ - content::Content, - images::{update_image_in_db, Image}, - model::{LibraryKind, Model}, - presentations::{update_presentation_in_db, Presentation}, - service_items::ServiceItem, - songs::{update_song_in_db, Song}, - videos::{update_video_in_db, Video}, +use crate::{ + core::{ + content::Content, + images::{update_image_in_db, Image}, + model::{LibraryKind, Model}, + presentations::{update_presentation_in_db, Presentation}, + service_items::ServiceItem, + songs::{update_song_in_db, Song}, + videos::{update_video_in_db, Video}, + }, + ui::widgets::icon, }; #[derive(Debug, Clone)] @@ -276,40 +279,40 @@ impl<'a> Library { where T: Content, { - let mut row = row::().spacing(5); + let mut row = row![].spacing(5); match &model.kind { LibraryKind::Song => { row = row .push(icon::from_name("folder-music-symbolic")); - row = row - .push(textm!("Songs").align_y(Vertical::Center)); + row = + row.push(text("Songs").align_y(Vertical::Center)); } LibraryKind::Video => { row = row .push(icon::from_name("folder-videos-symbolic")); row = row - .push(textm!("Videos").align_y(Vertical::Center)); + .push(text("Videos").align_y(Vertical::Center)); } LibraryKind::Image => { row = row.push(icon::from_name( "folder-pictures-symbolic", )); row = row - .push(textm!("Images").align_y(Vertical::Center)); + .push(text("Images").align_y(Vertical::Center)); } LibraryKind::Presentation => { row = row.push(icon::from_name( "x-office-presentation-symbolic", )); row = row.push( - textm!("Presentations").align_y(Vertical::Center), + text("Presentations").align_y(Vertical::Center), ); } }; let item_count = model.items.len(); row = row.push(horizontal_space()); row = row - .push(textm!("{}", item_count).align_y(Vertical::Center)); + .push(text!("{}", item_count).align_y(Vertical::Center)); row = row.push( icon::from_name({ if self.library_open == Some(model.kind) { @@ -329,21 +332,26 @@ impl<'a> Library { match self.library_hovered { Some(lib) => Background::Color( if lib == model.kind { - t.iced().button.hover.into() + t.extended_palette() + .primary + .strong + .color } else { - t.iced().button.base.into() + t.extended_palette() + .background + .base + .color }, ), None => Background::Color( - t.iced().button.base.into(), + t.extended_palette() + .background + .base + .color, ), } }) - .border( - Border::default().rounded( - t.iced().corner_radii.radius_s, - ), - ) + .border(Border::default().rounded(5)) }) .center_x(Length::Fill) .center_y(Length::Shrink); @@ -366,65 +374,34 @@ impl<'a> Library { let visual_item = self .single_item(index, item, model) .map(|_| Message::None); - DndSource::::new( - mouse_area(visual_item) - .on_drag(Message::DragItem(service_item.clone())) - .on_enter(Message::HoverItem( - Some(( - model.kind, - index as i32, - )), - )) - .on_double_click( - Message::OpenItem(Some(( - model.kind, - index as i32, - ))), - ) - .on_exit(Message::HoverItem(None)) - .on_press(Message::SelectItem( - Some(( - model.kind, - index as i32, - )), - )), - ) - .action(DndAction::Copy) - .drag_icon({ - let model = model.kind; - move |i| { - let state = State::None; - let icon = match model { - LibraryKind::Song => icon::from_name( - "folder-music-symbolic", - ).symbolic(true) - , - LibraryKind::Video => icon::from_name("folder-videos-symbolic"), - LibraryKind::Image => icon::from_name("folder-pictures-symbolic"), - LibraryKind::Presentation => icon::from_name("x-office-presentation-symbolic"), - }; - ( - icon.into(), - state, - i, - ) - }}) - .drag_content(move || { - service_item.to_owned() - }) - .into() + mouse_area(visual_item) + // .on_drag(Message::DragItem( + // service_item.clone(), + // )) + .on_enter(Message::HoverItem(Some(( + model.kind, + index as i32, + )))) + .on_double_click(Message::OpenItem( + Some((model.kind, index as i32)), + )) + .on_exit(Message::HoverItem(None)) + .on_press(Message::SelectItem(Some( + (model.kind, index as i32), + ))) + .into() }, ) }) - .spacing(2) - .width(Length::Fill), + .spacing(2) + .width(Length::Fill), ) - .spacing(5) - .height(Length::Fill); + .spacing(5) + .height(Length::Fill); - let library_toolbar = rowm!( + let library_toolbar = row!( text_input("Search...", ""), - button::icon(icon::from_name("add")) + button(icon::from_name("add")) ); let library_column = column![library_toolbar, items].spacing(3); @@ -445,33 +422,36 @@ impl<'a> Library { where T: Content, { - let text = Container::new(responsive(|size| { - text::heading(elide_text(item.title(), size.width)) + let item_text = Container::new(responsive(|size| { + text(elide_text(item.title(), size.width)) .center() - .wrapping(textm::Wrapping::None) + .wrapping(text::Wrapping::None) .into() })) .center_y(20) .center_x(Length::Fill); let subtext = container(responsive(|size| { - let color: Color = if item.background().is_some() { - theme::active().iced().accent_text_color().into() + if item.background().is_some() { + text(elide_text(item.subtext(), size.width)) + .style(text::primary) + .center() + .wrapping(text::Wrapping::None) + .into() } else { - theme::active().iced().destructive_text_color().into() - }; - text::body(elide_text(item.subtext(), size.width)) - .center() - .wrapping(textm::Wrapping::None) - .class(color) - .into() + text(elide_text(item.subtext(), size.width)) + .style(text::primary) + .center() + .wrapping(text::Wrapping::None) + .into() + } })) .center_y(20) .center_x(Length::Fill); - let texts = column([text.into(), subtext.into()]); + let texts = column([item_text.into(), subtext.into()]); Container::new( - rowm![horizontal_space().width(0), texts] + row![horizontal_space().width(0), texts] .spacing(10) .align_y(Vertical::Center), ) @@ -486,9 +466,9 @@ impl<'a> Library { if model.kind == library && selected == index as i32 { - t.iced().accent.selected.into() + t.extended_palette().primary.strong.color } else { - t.iced().button.base.into() + t.extended_palette().primary.base.color } } else if let Some((library, hovered)) = self.hovered_item @@ -496,18 +476,15 @@ impl<'a> Library { if model.kind == library && hovered == index as i32 { - t.iced().button.hover.into() + t.extended_palette().primary.strong.color } else { - t.iced().button.base.into() + t.extended_palette().primary.base.color } } else { - t.iced().button.base.into() + t.extended_palette().background.strong.color }, )) - .border( - Border::default() - .rounded(t.iced().corner_radii.radius_m), - ) + .border(Border::default().rounded(10)) }) .into() } diff --git a/src/ui/presenter.rs b/src/ui/presenter.rs index c0c8809..5f0333d 100644 --- a/src/ui/presenter.rs +++ b/src/ui/presenter.rs @@ -6,18 +6,15 @@ use iced::{ border, font::{Family, Stretch, Style, Weight}, widget::{ - container, image, mouse_area, responsive, scrollable, text, - Column, Container, Row, Space, - }, - widget::{ - rich_text, + container, image, mouse_area, responsive, rich_text, scrollable::{ - scroll_to, AbsoluteOffset, Direction, Scrollbar, + self, scroll_to, AbsoluteOffset, Direction, Id, Scrollbar, }, - span, stack, vertical_rule, + span, stack, text, vertical_rule, Column, Container, Row, + Space, }, - Background, Border, Color, ContentFit, Font, Length, Shadow, - Task, Vector, + Background, Border, Color, ContentFit, Element, Font, Length, + Shadow, Task, Vector, }; use iced_video_player::{Position, Video, VideoPlayer}; use rodio::{Decoder, OutputStream, Sink}; @@ -164,7 +161,7 @@ impl Presenter { ) }, scroll_id: Id::unique(), - current_font: iced::font::default(), + current_font: iced::font::Font::DEFAULT, } } @@ -340,27 +337,27 @@ impl Presenter { return Action::Task(Task::perform( async move { tokio::task::spawn_blocking(move || { - match gst_pbutils::MissingPluginMessage::parse(&element) { - Ok(missing_plugin) => { - let mut install_ctx = gst_pbutils::InstallPluginsContext::new(); - install_ctx - .set_desktop_id(&format!("{}.desktop", "org.chriscochrun.lumina")); - let install_detail = missing_plugin.installer_detail(); - println!("installing plugins: {}", install_detail); - let status = gst_pbutils::missing_plugins::install_plugins_sync( - &[&install_detail], - Some(&install_ctx), - ); - info!("plugin install status: {}", status); - info!( - "gstreamer registry update: {:?}", - gstreamer::Registry::update() - ); - } - Err(err) => { - warn!("failed to parse missing plugin message: {err}"); - } - } + // match gst_pbutils::MissingPluginMessage::parse(&element) { + // Ok(missing_plugin) => { + // let mut install_ctx = gst_pbutils::InstallPluginsContext::new(); + // install_ctx + // .set_desktop_id(&format!("{}.desktop", "org.chriscochrun.lumina")); + // let install_detail = missing_plugin.installer_detail(); + // println!("installing plugins: {}", install_detail); + // let status = gst_pbutils::missing_plugins::install_plugins_sync( + // &[&install_detail], + // Some(&install_ctx), + // ); + // info!("plugin install status: {}", status); + // info!( + // "gstreamer registry update: {:?}", + // gstreamer::Registry::update() + // ); + // } + // Err(err) => { + // warn!("failed to parse missing plugin message: {err}"); + // } + // } Message::None }) .await @@ -445,7 +442,7 @@ impl Presenter { .style(move |t| { let mut style = container::Style::default(); - let theme = t.iced(); + let theme = t; let hovered = self.hovered_slide == Some(( item_index, @@ -455,19 +452,25 @@ impl Presenter { Some(Background::Color( if is_current_slide { theme - .accent - .base - .into() + .extended_palette( + ) + .secondary + .strong + .color } else if hovered { theme - .accent - .hover - .into() + .extended_palette( + ) + .secondary + .strong + .color } else { theme - .palette - .neutral_3 - .into() + .extended_palette( + ) + .background + .neutral + .color }, )); style.border = Border::default() @@ -500,7 +503,7 @@ impl Presenter { .padding(10), ) .interaction( - iced::iced::mouse::Interaction::Pointer, + iced::mouse::Interaction::Pointer, ) .on_move(move |_| { Message::HoveredSlide(Some(( @@ -518,11 +521,11 @@ impl Presenter { let row = Row::from_vec(slides) .spacing(10) .padding([20, 15]); - let label = text::body(item.title.clone()); + let label = text(item.title.clone()); let label_container = container(label) .align_top(Length::Fill) .align_left(Length::Fill) - .padding([0, 0, 0, 35]); + .padding([0, 35]); let divider = vertical_rule(2); items.push( container(stack!(row, label_container)) @@ -532,15 +535,16 @@ impl Presenter { items.push(divider.into()); }, ); - let row = - scrollable(container(Row::from_vec(items)).style(|t| { + let row = scrollable::Scrollable::new( + container(Row::from_vec(items)).style(|t| { let style = container::Style::default(); style.border(Border::default().width(2)) - })) - .direction(Direction::Horizontal(Scrollbar::new())) - .height(Length::Fill) - .width(Length::Fill) - .id(self.scroll_id.clone()); + }), + ) + .direction(Direction::Horizontal(Scrollbar::new())) + .height(Length::Fill) + .width(Length::Fill) + .id(self.scroll_id.clone()); row.into() } @@ -819,15 +823,15 @@ pub(crate) fn slide_view( } else if let Some(video) = &video { Container::new( VideoPlayer::new(video) - .mouse_hidden(hide_mouse) + // .mouse_hidden(hide_mouse) .width(width) .height(size.height) .on_end_of_stream(Message::EndVideo) .on_new_frame(Message::VideoFrame) - .on_missing_plugin(Message::MissingPlugin) - .on_warning(|w| { - Message::Error(w.to_string()) - }) + // .on_missing_plugin(Message::MissingPlugin) + // .on_warning(|w| { + // Message::Error(w.to_string()) + // }) .on_error(|e| { Message::Error(e.to_string()) }) diff --git a/src/ui/slide_editor.rs b/src/ui/slide_editor.rs index 36dcddf..94fc282 100644 --- a/src/ui/slide_editor.rs +++ b/src/ui/slide_editor.rs @@ -49,7 +49,7 @@ pub enum SlideError { #[derive(Debug, Default)] struct EditorProgram { - mouse_button_pressed: Option, + mouse_button_pressed: Option, } impl SlideEditor { @@ -78,8 +78,8 @@ impl<'a> Program state: &Self::State, renderer: &Renderer, theme: &iced::Theme, - bounds: iced::iced::Rectangle, - cursor: iced::iced_core::mouse::Cursor, + bounds: iced::Rectangle, + cursor: iced::mouse::Cursor, ) -> Vec> { // We prepare a new `Frame` let mut frame = canvas::Frame::new(renderer, bounds.size()); @@ -88,7 +88,7 @@ impl<'a> Program // We create a `Path` representing a simple circle let circle = canvas::Path::circle(frame.center(), 50.0); let border = canvas::Path::rectangle( - iced::iced::Point { x: 10.0, y: 10.0 }, + iced::Point { x: 10.0, y: 10.0 }, Size::new(frame_rect.width, frame_rect.height), ); @@ -114,21 +114,19 @@ impl<'a> Program fn update( &self, _state: &mut Self::State, - event: canvas::Event, - bounds: iced::iced::Rectangle, - _cursor: iced::iced_core::mouse::Cursor, - ) -> (canvas::event::Status, Option) { + event: &iced::Event, + bounds: iced::Rectangle, + _cursor: iced::mouse::Cursor, + ) -> std::option::Option> { match event { - canvas::Event::Mouse(event) => match event { - iced::iced::mouse::Event::CursorEntered => { + iced::Event::Mouse(event) => match event { + iced::mouse::Event::CursorEntered => { debug!("cursor entered") } - iced::iced::mouse::Event::CursorLeft => { + iced::mouse::Event::CursorLeft => { debug!("cursor left") } - iced::iced::mouse::Event::CursorMoved { - position, - } => { + iced::mouse::Event::CursorMoved { position } => { if bounds.x < position.x && bounds.y < position.y && (bounds.width + bounds.x) > position.x @@ -137,29 +135,34 @@ impl<'a> Program debug!(?position, "cursor moved"); } } - iced::iced::mouse::Event::ButtonPressed(button) => { + iced::mouse::Event::ButtonPressed(button) => { // self.mouse_button_pressed = Some(button); debug!(?button, "mouse button pressed") } - iced::iced::mouse::Event::ButtonReleased(button) => { + iced::mouse::Event::ButtonReleased(button) => { debug!(?button, "mouse button released") } - iced::iced::mouse::Event::WheelScrolled { delta } => { + iced::mouse::Event::WheelScrolled { delta } => { debug!(?delta, "scroll wheel") } }, - canvas::Event::Touch(event) => debug!("test"), - canvas::Event::Keyboard(event) => debug!("test"), + iced::Event::Touch(event) => debug!("test"), + iced::Event::Keyboard(event) => debug!("test"), + iced::Event::Keyboard(event) => todo!(), + iced::Event::Mouse(event) => todo!(), + iced::Event::Window(event) => todo!(), + iced::Event::Touch(event) => todo!(), + iced::Event::InputMethod(event) => todo!(), } - (canvas::event::Status::Ignored, None) + None } fn mouse_interaction( &self, _state: &Self::State, - _bounds: iced::iced::Rectangle, - _cursor: iced::iced_core::mouse::Cursor, - ) -> iced::iced_core::mouse::Interaction { - iced::iced_core::mouse::Interaction::default() + _bounds: iced::Rectangle, + _cursor: iced::mouse::Cursor, + ) -> iced::mouse::Interaction { + iced::mouse::Interaction::default() } } diff --git a/src/ui/song_editor.rs b/src/ui/song_editor.rs index d9e64b5..21e0cdb 100644 --- a/src/ui/song_editor.rs +++ b/src/ui/song_editor.rs @@ -5,19 +5,22 @@ use iced::{ advanced::graphics::text::cosmic_text::fontdb, font::{Family, Stretch, Style, Weight}, theme, - widget::row, widget::{ - button, column, combo_box, container, horizontal_space, - scrollable, text, text_editor, text_input, + button, column, combo_box, container, horizontal_space, row, + scrollable, text, text_editor, text_input, tooltip, }, Element, Font, Length, Task, }; use iced_video_player::Video; +use rfd::AsyncFileDialog; use tracing::{debug, error}; use crate::{ core::{service_items::ServiceTrait, songs::Song}, - ui::slide_editor::{self, SlideEditor}, + ui::{ + slide_editor::{self, SlideEditor}, + widgets::icon, + }, Background, BackgroundKind, }; @@ -129,7 +132,7 @@ impl SongEditor { audio: PathBuf::new(), background: None, video: None, - current_font: iced::font::default(), + current_font: iced::font::Font::DEFAULT, ccli: "8".to_owned(), slide_state: SlideEditor::default(), } @@ -269,7 +272,7 @@ impl SongEditor { let slide_preview = container(self.slide_preview()) .width(Length::FillPortion(2)); - let column = column::with_children(vec![ + let column = column![ self.toolbar(), row![ container(self.left_column()) @@ -278,8 +281,8 @@ impl SongEditor { .center_x(Length::FillPortion(3)) ] .into(), - ]) - .spacing(theme::active().iced().space_l()); + ] + .spacing(15); column.into() } @@ -332,37 +335,34 @@ impl SongEditor { fn left_column(&self) -> Element { let title_input = text_input("song", &self.title) - .on_input(Message::ChangeTitle) - .label("Song Title"); + .on_input(Message::ChangeTitle); let author_input = text_input("author", &self.author) - .on_input(Message::ChangeAuthor) - .label("Song Author"); + .on_input(Message::ChangeAuthor); let verse_input = text_input( "Verse order", &self.verse_order, ) - .label("Verse Order") .on_input(Message::ChangeVerseOrder); let lyric_title = text("Lyrics"); - let lyric_input = column::with_children(vec![ + let lyric_input = column![ lyric_title.into(), text_editor(&self.lyrics) .on_action(Message::ChangeLyrics) .height(Length::Fill) .into(), - ]) + ] .spacing(5); - column::with_children(vec![ + column![ title_input.into(), author_input.into(), verse_input.into(), lyric_input.into(), - ]) + ] .spacing(25) .width(Length::FillPortion(2)) .into() @@ -397,16 +397,20 @@ order", ) }, ) - .width(theme::active().iced().space_xxl()); + .width(200); - let background_selector = button::icon( + let background_selector = button(row!( icon::from_name("folder-pictures-symbolic").scale(2), - ) - .label("Background") - .tooltip("Select an image or video background") + "Background" + )) .on_press(Message::PickBackground) .padding(10); + let background_selector = tooltip( + background_selector, + "Select an image or video background", + tooltip::Position::FollowCursor, + ); row![ font_selector, font_size, @@ -448,18 +452,19 @@ impl Default for SongEditor { } async fn pick_background() -> Result { - let dialog = Dialog::new().title("Choose a background..."); - dialog - .open_file() - .await - .map_err(|_| SongError::DialogClosed) - .map(|file| file.url().to_file_path().unwrap()) - // rfd::AsyncFileDialog::new() - // .set_title("Choose a background...") + // let dialog = + // AsyncFileDialog::new().set_title("Choose a background..."); + // dialog // .pick_file() // .await - // .ok_or(SongError::DialogClosed) - // .map(|file| file.path().to_owned()) + // .map_err(|_| SongError::DialogClosed) + // .map(|file| file.url().to_file_path().unwrap()) + rfd::AsyncFileDialog::new() + .set_title("Choose a background...") + .pick_file() + .await + .ok_or(SongError::DialogClosed) + .map(|file| file.path().to_owned()) } #[derive(Debug, Clone)] diff --git a/src/ui/text_svg.rs b/src/ui/text_svg.rs index 728478e..d420123 100644 --- a/src/ui/text_svg.rs +++ b/src/ui/text_svg.rs @@ -7,7 +7,7 @@ use colors_transform::Rgb; use iced::{ font::{Style, Weight}, widget::{container, svg::Handle, Svg}, - Length, Size, + Element, Length, Size, }; use tracing::error; @@ -47,9 +47,7 @@ impl From for Font { fn from(value: iced::font::Font) -> Self { Self { name: match value.family { - iced::iced::font::Family::Name(name) => { - name.to_string() - } + iced::font::Family::Name(name) => name.to_string(), _ => "Quicksand Bold".into(), }, size: 20, diff --git a/src/ui/widgets/icon/handle.rs b/src/ui/widgets/icon/handle.rs new file mode 100644 index 0000000..efceb0a --- /dev/null +++ b/src/ui/widgets/icon/handle.rs @@ -0,0 +1,115 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +use super::{Icon, Named}; +use iced::widget::{image, svg}; +use std::borrow::Cow; +use std::ffi::OsStr; +use std::hash::Hash; +use std::path::PathBuf; + +#[must_use] +#[derive(Clone, Debug, derive_setters::Setters)] +pub struct Handle { + pub symbolic: bool, + #[setters(skip)] + pub data: Data, +} + +impl Handle { + #[inline] + pub fn icon(self) -> Icon { + super::icon(self) + } +} + +#[must_use] +#[derive(Clone, Debug)] +pub enum Data { + Name(Named), + Image(image::Handle), + Svg(svg::Handle), +} + +/// Create an icon handle from its path. +pub fn from_path(path: PathBuf) -> Handle { + Handle { + symbolic: path + .file_stem() + .and_then(OsStr::to_str) + .is_some_and(|name| name.ends_with("-symbolic")), + data: if path + .extension() + .is_some_and(|ext| ext == OsStr::new("svg")) + { + Data::Svg(svg::Handle::from_path(path)) + } else { + Data::Image(image::Handle::from_path(path)) + }, + } +} + +/// Create an image handle from memory. +pub fn from_raster_bytes( + bytes: impl Into> + + std::convert::AsRef<[u8]> + + std::marker::Send + + std::marker::Sync + + 'static, +) -> Handle { + fn inner(bytes: Cow<'static, [u8]>) -> Handle { + Handle { + symbolic: false, + data: match bytes { + Cow::Owned(b) => { + Data::Image(image::Handle::from_bytes(b)) + } + Cow::Borrowed(b) => { + Data::Image(image::Handle::from_bytes(b)) + } + }, + } + } + + inner(bytes.into()) +} + +/// Create an image handle from RGBA data, where you must define the width and height. +pub fn from_raster_pixels( + width: u32, + height: u32, + pixels: impl Into> + + std::convert::AsRef<[u8]> + + std::marker::Send + + std::marker::Sync, +) -> Handle { + fn inner( + width: u32, + height: u32, + pixels: Cow<'static, [u8]>, + ) -> Handle { + Handle { + symbolic: false, + data: match pixels { + Cow::Owned(pixels) => Data::Image( + image::Handle::from_rgba(width, height, pixels), + ), + Cow::Borrowed(pixels) => Data::Image( + image::Handle::from_rgba(width, height, pixels), + ), + }, + } + } + + inner(width, height, pixels.into()) +} + +/// Create a SVG handle from memory. +pub fn from_svg_bytes( + bytes: impl Into>, +) -> Handle { + Handle { + symbolic: false, + data: Data::Svg(svg::Handle::from_memory(bytes)), + } +} diff --git a/src/ui/widgets/icon/mod.rs b/src/ui/widgets/icon/mod.rs new file mode 100644 index 0000000..ac6fba3 --- /dev/null +++ b/src/ui/widgets/icon/mod.rs @@ -0,0 +1,187 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + +//! Lazily-generated SVG icon widget for Iced. + +mod named; +use std::ffi::OsStr; +use std::sync::Arc; + +pub use named::{IconFallback, Named}; + +mod handle; +pub use handle::{ + from_path, from_raster_bytes, from_raster_pixels, from_svg_bytes, + Data, Handle, +}; + +use derive_setters::Setters; +use iced::advanced::{image, svg}; +use iced::widget::{Image, Svg}; +use iced::Element; +use iced::Rotation; +use iced::{ContentFit, Length, Rectangle}; + +/// Create an [`Icon`] from a pre-existing [`Handle`] +pub fn icon(handle: Handle) -> Icon { + Icon { + content_fit: ContentFit::Fill, + handle, + height: None, + size: 16, + rotation: None, + width: None, + } +} + +/// Create an icon handle from its XDG icon name. +pub fn from_name(name: impl Into>) -> Named { + Named::new(name) +} + +/// An image which may be an SVG or PNG. +#[must_use] +#[derive(Clone, Setters)] +pub struct Icon { + #[setters(skip)] + handle: Handle, + pub(super) size: u16, + content_fit: ContentFit, + #[setters(strip_option)] + width: Option, + #[setters(strip_option)] + height: Option, + #[setters(strip_option)] + rotation: Option, +} + +impl Icon { + #[must_use] + pub fn into_svg_handle( + self, + ) -> Option { + match self.handle.data { + Data::Name(named) => { + if let Some(path) = named.path() { + if path + .extension() + .is_some_and(|ext| ext == OsStr::new("svg")) + { + return Some( + iced::advanced::svg::Handle::from_path( + path, + ), + ); + } + } + } + + Data::Image(_) => (), + Data::Svg(handle) => return Some(handle), + } + + None + } + + #[must_use] + fn view<'a, Message: 'a>(self) -> Element<'a, Message> { + let from_image = |handle| { + Image::new(handle) + .width(self.width.unwrap_or_else(|| { + Length::Fixed(f32::from(self.size)) + })) + .height(self.height.unwrap_or_else(|| { + Length::Fixed(f32::from(self.size)) + })) + .rotation(self.rotation.unwrap_or_default()) + .content_fit(self.content_fit) + .into() + }; + + let from_svg = |handle| { + Svg::::new(handle) + .width(self.width.unwrap_or_else(|| { + Length::Fixed(f32::from(self.size)) + })) + .height(self.height.unwrap_or_else(|| { + Length::Fixed(f32::from(self.size)) + })) + .rotation(self.rotation.unwrap_or_default()) + .content_fit(self.content_fit) + .into() + }; + + match self.handle.data { + Data::Name(named) => { + if let Some(path) = named.path() { + if path + .extension() + .is_some_and(|ext| ext == OsStr::new("svg")) + { + from_svg(svg::Handle::from_path(path)) + } else { + from_image(image::Handle::from_path(path)) + } + } else { + let bytes: &'static [u8] = &[]; + from_svg(svg::Handle::from_memory(bytes)) + } + } + + Data::Image(handle) => from_image(handle), + Data::Svg(handle) => from_svg(handle), + } + } +} + +impl<'a, Message: 'a> From for Element<'a, Message> { + fn from(icon: Icon) -> Self { + icon.view::() + } +} + +/// Draw an icon in the given bounds via the runtime's renderer. +pub fn draw( + renderer: &mut iced::Renderer, + handle: &Handle, + icon_bounds: Rectangle, +) { + enum IcedHandle { + Svg(svg::Handle), + Image(image::Handle), + } + + let iced_handle = match handle.clone().data { + Data::Name(named) => named.path().map(|path| { + if path + .extension() + .is_some_and(|ext| ext == OsStr::new("svg")) + { + IcedHandle::Svg(svg::Handle::from_path(path)) + } else { + IcedHandle::Image(image::Handle::from_path(path)) + } + }), + + Data::Image(handle) => Some(IcedHandle::Image(handle)), + Data::Svg(handle) => Some(IcedHandle::Svg(handle)), + }; + + match iced_handle { + Some(IcedHandle::Svg(handle)) => svg::Renderer::draw_svg( + renderer, + svg::Svg::new(handle), + icon_bounds, + ), + + Some(IcedHandle::Image(handle)) => { + image::Renderer::draw_image( + renderer, + (&handle).into(), + icon_bounds, + ); + } + + None => {} + } +} diff --git a/src/ui/widgets/icon/named.rs b/src/ui/widgets/icon/named.rs new file mode 100644 index 0000000..54ee390 --- /dev/null +++ b/src/ui/widgets/icon/named.rs @@ -0,0 +1,165 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +use super::{Handle, Icon}; +use std::{borrow::Cow, path::PathBuf, sync::Arc}; + +#[derive(Debug, Clone, Default, Hash)] +/// Fallback icon to use if the icon was not found. +pub enum IconFallback { + #[default] + /// Default fallback using the icon name. + Default, + /// Fallback to specific icon names. + Names(Vec>), +} + +#[must_use] +#[derive(derive_setters::Setters, Clone, Debug, Hash)] +pub struct Named { + /// Name of icon to locate in an XDG icon path. + pub(super) name: Arc, + + /// Checks for a fallback if the icon was not found. + pub fallback: Option, + + /// Restrict the lookup to a given scale. + #[setters(strip_option)] + pub scale: Option, + + /// Restrict the lookup to a given size. + #[setters(strip_option)] + pub size: Option, + + /// Whether the icon is symbolic or not. + pub symbolic: bool, + + /// Prioritizes SVG over PNG + pub prefer_svg: bool, +} + +impl Named { + pub fn new(name: impl Into>) -> Self { + let name = name.into(); + Self { + symbolic: name.ends_with("-symbolic"), + name, + fallback: Some(IconFallback::Default), + size: None, + scale: None, + prefer_svg: false, + } + } + + #[cfg(not(windows))] + #[must_use] + pub fn path(self) -> Option { + let name = &*self.name; + let fallback = &self.fallback; + let locate = |theme: &str, name| { + let mut lookup = freedesktop_icons::lookup(name) + .with_theme(theme.as_ref()) + .with_cache(); + + if let Some(scale) = self.scale { + lookup = lookup.with_scale(scale); + } + + if let Some(size) = self.size { + lookup = lookup.with_size(size); + } + + if self.prefer_svg { + lookup = lookup.force_svg(); + } + lookup.find() + }; + + let theme = "Papirus-Dark"; + let themes = if theme.as_ref() == "Cosmic" { + vec![theme.as_ref()] + } else { + vec![theme.as_ref(), "Cosmic"] + }; + + let mut result = themes.iter().find_map(|t| locate(t, name)); + + // On failure, attempt to locate fallback icon. + if result.is_none() { + if matches!(fallback, Some(IconFallback::Default)) { + for new_name in name + .rmatch_indices('-') + .map(|(pos, _)| &name[..pos]) + { + result = themes + .iter() + .find_map(|t| locate(t, new_name)); + if result.is_some() { + break; + } + } + } else if let Some(IconFallback::Names(fallbacks)) = + fallback + { + for fallback in fallbacks { + result = themes + .iter() + .find_map(|t| locate(t, fallback)); + if result.is_some() { + break; + } + } + } + } + + result + } + + #[cfg(windows)] + #[must_use] + pub fn path(self) -> Option { + //TODO: implement icon lookup for Windows + None + } + + #[inline] + pub fn handle(self) -> Handle { + Handle { + symbolic: self.symbolic, + data: super::Data::Name(self), + } + } + + #[inline] + pub fn icon(self) -> Icon { + let size = self.size; + + let icon = super::icon(self.handle()); + + match size { + Some(size) => icon.size(size), + None => icon, + } + } +} + +impl From for Handle { + #[inline] + fn from(builder: Named) -> Self { + builder.handle() + } +} + +impl From for Icon { + #[inline] + fn from(builder: Named) -> Self { + builder.icon() + } +} + +impl From for crate::Element<'_, Message> { + #[inline] + fn from(builder: Named) -> Self { + builder.icon().into() + } +} diff --git a/src/ui/widgets/mod.rs b/src/ui/widgets/mod.rs index d786591..f88ea38 100644 --- a/src/ui/widgets/mod.rs +++ b/src/ui/widgets/mod.rs @@ -1 +1,2 @@ // pub mod slide_text; +pub mod icon;