parent
4ae6a9a9a7
commit
1861f357a8
16 changed files with 1026 additions and 562 deletions
98
Cargo.lock
generated
98
Cargo.lock
generated
|
@ -1496,6 +1496,41 @@ dependencies = [
|
||||||
"winreg 0.52.0",
|
"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]]
|
[[package]]
|
||||||
name = "dasp_sample"
|
name = "dasp_sample"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
|
@ -1545,6 +1580,18 @@ dependencies = [
|
||||||
"syn 1.0.109",
|
"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]]
|
[[package]]
|
||||||
name = "detect-desktop-environment"
|
name = "detect-desktop-environment"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
@ -1969,6 +2016,12 @@ dependencies = [
|
||||||
"spin",
|
"spin",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fnv"
|
||||||
|
version = "1.0.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "foldhash"
|
name = "foldhash"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
|
@ -2066,6 +2119,20 @@ dependencies = [
|
||||||
"percent-encoding",
|
"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]]
|
[[package]]
|
||||||
name = "futures"
|
name = "futures"
|
||||||
version = "0.3.31"
|
version = "0.3.31"
|
||||||
|
@ -2440,7 +2507,7 @@ dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"presser",
|
"presser",
|
||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
"windows 0.54.0",
|
"windows 0.58.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -3239,6 +3306,12 @@ dependencies = [
|
||||||
"zerovec",
|
"zerovec",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ident_case"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "1.0.3"
|
version = "1.0.3"
|
||||||
|
@ -3333,6 +3406,15 @@ dependencies = [
|
||||||
"hashbrown 0.15.5",
|
"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]]
|
[[package]]
|
||||||
name = "instant"
|
name = "instant"
|
||||||
version = "0.1.13"
|
version = "0.1.13"
|
||||||
|
@ -3730,7 +3812,9 @@ dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"colors-transform",
|
"colors-transform",
|
||||||
"crisp",
|
"crisp",
|
||||||
|
"derive_setters",
|
||||||
"dirs 5.0.1",
|
"dirs 5.0.1",
|
||||||
|
"freedesktop-icons",
|
||||||
"gstreamer",
|
"gstreamer",
|
||||||
"gstreamer-app",
|
"gstreamer-app",
|
||||||
"iced 0.14.0-dev",
|
"iced 0.14.0-dev",
|
||||||
|
@ -7385,7 +7469,7 @@ dependencies = [
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"log",
|
"log",
|
||||||
"naga 0.19.2",
|
"naga 0.19.2",
|
||||||
"parking_lot 0.11.2",
|
"parking_lot 0.12.4",
|
||||||
"profiling",
|
"profiling",
|
||||||
"raw-window-handle 0.6.2",
|
"raw-window-handle 0.6.2",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
|
@ -7442,7 +7526,7 @@ dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"naga 0.19.2",
|
"naga 0.19.2",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"parking_lot 0.11.2",
|
"parking_lot 0.12.4",
|
||||||
"profiling",
|
"profiling",
|
||||||
"raw-window-handle 0.6.2",
|
"raw-window-handle 0.6.2",
|
||||||
"rustc-hash 1.1.0",
|
"rustc-hash 1.1.0",
|
||||||
|
@ -7542,7 +7626,7 @@ dependencies = [
|
||||||
"ndk-sys 0.5.0+25.2.9519653",
|
"ndk-sys 0.5.0+25.2.9519653",
|
||||||
"objc",
|
"objc",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"parking_lot 0.11.2",
|
"parking_lot 0.12.4",
|
||||||
"profiling",
|
"profiling",
|
||||||
"range-alloc",
|
"range-alloc",
|
||||||
"raw-window-handle 0.6.2",
|
"raw-window-handle 0.6.2",
|
||||||
|
@ -8342,6 +8426,12 @@ version = "0.3.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b"
|
checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "xdg"
|
||||||
|
version = "2.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "xdg-home"
|
name = "xdg-home"
|
||||||
version = "1.3.0"
|
version = "1.3.0"
|
||||||
|
|
|
@ -38,6 +38,8 @@ rayon = "1.11.0"
|
||||||
# wgpu = "26.0.1"
|
# wgpu = "26.0.1"
|
||||||
# mupdf = "0.5.0"
|
# mupdf = "0.5.0"
|
||||||
rfd = { version = "0.12.1", features = ["xdg-portal"], default-features = false }
|
rfd = { version = "0.12.1", features = ["xdg-portal"], default-features = false }
|
||||||
|
derive_setters = "0.1.8"
|
||||||
|
freedesktop-icons = "0.4.0"
|
||||||
|
|
||||||
[dependencies.iced]
|
[dependencies.iced]
|
||||||
git = "https://github.com/iced-rs/iced"
|
git = "https://github.com/iced-rs/iced"
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
use std::mem::replace;
|
use std::mem::replace;
|
||||||
|
|
||||||
use iced::iced::Executor;
|
|
||||||
use miette::{miette, Result};
|
use miette::{miette, Result};
|
||||||
use sqlx::{Connection, SqliteConnection};
|
use sqlx::{Connection, SqliteConnection};
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ use std::ops::Deref;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crisp::types::{Keyword, Symbol, Value};
|
use crisp::types::{Keyword, Symbol, Value};
|
||||||
use iced::iced::clipboard::mime::{AllowedMimeTypes, AsMimeTypes};
|
// use cosmic::iced::clipboard::mime::{AllowedMimeTypes, AsMimeTypes};
|
||||||
use miette::Result;
|
use miette::Result;
|
||||||
use tracing::{debug, error};
|
use tracing::{debug, error};
|
||||||
|
|
||||||
|
@ -56,29 +56,29 @@ impl TryFrom<(Vec<u8>, String)> for ServiceItem {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AllowedMimeTypes for ServiceItem {
|
// impl AllowedMimeTypes for ServiceItem {
|
||||||
fn allowed() -> Cow<'static, [String]> {
|
// fn allowed() -> Cow<'static, [String]> {
|
||||||
Cow::from(vec!["application/service-item".to_string()])
|
// Cow::from(vec!["application/service-item".to_string()])
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
impl AsMimeTypes for ServiceItem {
|
// impl AsMimeTypes for ServiceItem {
|
||||||
fn available(&self) -> Cow<'static, [String]> {
|
// fn available(&self) -> Cow<'static, [String]> {
|
||||||
debug!(?self);
|
// debug!(?self);
|
||||||
Cow::from(vec!["application/service-item".to_string()])
|
// Cow::from(vec!["application/service-item".to_string()])
|
||||||
}
|
// }
|
||||||
|
|
||||||
fn as_bytes(
|
// fn as_bytes(
|
||||||
&self,
|
// &self,
|
||||||
mime_type: &str,
|
// mime_type: &str,
|
||||||
) -> Option<std::borrow::Cow<'static, [u8]>> {
|
// ) -> Option<std::borrow::Cow<'static, [u8]>> {
|
||||||
debug!(?self);
|
// debug!(?self);
|
||||||
debug!(mime_type);
|
// debug!(mime_type);
|
||||||
let val = Value::from(self);
|
// let val = Value::from(self);
|
||||||
let val = String::from(val);
|
// let val = String::from(val);
|
||||||
Some(Cow::from(val.into_bytes()))
|
// Some(Cow::from(val.into_bytes()))
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
impl From<&ServiceItem> for Value {
|
impl From<&ServiceItem> for Value {
|
||||||
fn from(value: &ServiceItem) -> Self {
|
fn from(value: &ServiceItem) -> Self {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
use std::{collections::HashMap, option::Option, path::PathBuf};
|
use std::{collections::HashMap, option::Option, path::PathBuf};
|
||||||
|
|
||||||
use crisp::types::{Keyword, Symbol, Value};
|
use crisp::types::{Keyword, Symbol, Value};
|
||||||
use iced::iced::Executor;
|
|
||||||
use miette::{miette, IntoDiagnostic, Result};
|
use miette::{miette, IntoDiagnostic, Result};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::{
|
use sqlx::{
|
||||||
|
|
|
@ -8,7 +8,6 @@ use super::{
|
||||||
slide::Slide,
|
slide::Slide,
|
||||||
};
|
};
|
||||||
use crisp::types::{Keyword, Symbol, Value};
|
use crisp::types::{Keyword, Symbol, Value};
|
||||||
use iced::iced::Executor;
|
|
||||||
use miette::{IntoDiagnostic, Result};
|
use miette::{IntoDiagnostic, Result};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::{
|
use sqlx::{
|
||||||
|
|
500
src/main.rs
500
src/main.rs
|
@ -7,8 +7,8 @@ use iced::keyboard::{Key, Modifiers};
|
||||||
use iced::theme::{self, Palette};
|
use iced::theme::{self, Palette};
|
||||||
use iced::widget::tooltip::Position as TPosition;
|
use iced::widget::tooltip::Position as TPosition;
|
||||||
use iced::widget::{
|
use iced::widget::{
|
||||||
button, horizontal_space, slider, text, tooltip, vertical_space,
|
button, horizontal_space, mouse_area, slider, text, text_input,
|
||||||
Space,
|
tooltip, vertical_space, Space,
|
||||||
};
|
};
|
||||||
use iced::widget::{column, row};
|
use iced::widget::{column, row};
|
||||||
use iced::window::{Mode, Position};
|
use iced::window::{Mode, Position};
|
||||||
|
@ -31,6 +31,8 @@ use ui::presenter::{self, Presenter};
|
||||||
use ui::song_editor::{self, SongEditor};
|
use ui::song_editor::{self, SongEditor};
|
||||||
use ui::EditorMode;
|
use ui::EditorMode;
|
||||||
|
|
||||||
|
use crate::ui::widgets::icon;
|
||||||
|
|
||||||
pub mod core;
|
pub mod core;
|
||||||
pub mod lisp;
|
pub mod lisp;
|
||||||
pub mod ui;
|
pub mod ui;
|
||||||
|
@ -62,26 +64,24 @@ fn main() -> Result<()> {
|
||||||
.with_timer(timer)
|
.with_timer(timer)
|
||||||
.init();
|
.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;
|
iced::daemon(App::init, App::update, App::view)
|
||||||
if args.ui {
|
.settings(Settings::default())
|
||||||
debug!("main view");
|
.subscription(App::subscription)
|
||||||
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)
|
|
||||||
.theme(App::theme)
|
.theme(App::theme)
|
||||||
.title(App::title)
|
.title(App::title)
|
||||||
.run()
|
.run()
|
||||||
|
@ -128,10 +128,10 @@ enum Message {
|
||||||
ChangeServiceItem(usize),
|
ChangeServiceItem(usize),
|
||||||
AddServiceItem(usize, ServiceItem),
|
AddServiceItem(usize, ServiceItem),
|
||||||
AddServiceItemDrop(usize),
|
AddServiceItemDrop(usize),
|
||||||
AppendServiceItem(ServiceItem),
|
AppendServiceItem(Option<ServiceItem>),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone)]
|
||||||
struct Window {
|
struct Window {
|
||||||
title: String,
|
title: String,
|
||||||
scale_input: String,
|
scale_input: String,
|
||||||
|
@ -145,31 +145,66 @@ impl Default for Window {
|
||||||
title: Default::default(),
|
title: Default::default(),
|
||||||
scale_input: Default::default(),
|
scale_input: Default::default(),
|
||||||
current_scale: 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 {
|
impl App {
|
||||||
const APP_ID: &'static str = "lumina";
|
const APP_ID: &'static str = "lumina";
|
||||||
fn init(input: Cli) -> (Self, Task<Self::Message>) {
|
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<Message>) {
|
||||||
debug!("init");
|
debug!("init");
|
||||||
|
let args = Cli::parse();
|
||||||
|
|
||||||
let mut batch = vec![];
|
let mut batch = vec![];
|
||||||
let mut windows = BTreeMap::new();
|
let mut windows = BTreeMap::new();
|
||||||
if input.ui {
|
if args.ui {
|
||||||
let settings = window::Settings {
|
let settings = window::Settings {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let (id, open) = window::open(settings);
|
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) => {
|
Ok(lisp) => {
|
||||||
let mut slide_vector = vec![];
|
let mut slide_vector = vec![];
|
||||||
let lisp = crisp::reader::read(&lisp);
|
let lisp = crisp::reader::read(&lisp);
|
||||||
|
@ -210,7 +245,7 @@ impl App {
|
||||||
file: PathBuf::default(),
|
file: PathBuf::default(),
|
||||||
windows,
|
windows,
|
||||||
presentation_open: false,
|
presentation_open: false,
|
||||||
cli_mode: !input.ui,
|
cli_mode: !args.ui,
|
||||||
library: None,
|
library: None,
|
||||||
library_open: true,
|
library_open: true,
|
||||||
library_width: 60.0,
|
library_width: 60.0,
|
||||||
|
@ -221,9 +256,9 @@ impl App {
|
||||||
library_dragged_item: None,
|
library_dragged_item: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
if input.ui {
|
if args.ui {
|
||||||
debug!("main view");
|
debug!("main view");
|
||||||
batch.push(app.update_title())
|
// batch.push(app.update_title())
|
||||||
} else {
|
} else {
|
||||||
debug!("window view");
|
debug!("window view");
|
||||||
batch.push(app.show_window())
|
batch.push(app.show_window())
|
||||||
|
@ -236,9 +271,9 @@ impl App {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn nav_bar(&self) -> Option<Element<Message>> {
|
fn nav_bar(&self) -> Option<Element<Message>> {
|
||||||
if !self.core().nav_bar_active() {
|
// if !self.core().nav_bar_active() {
|
||||||
return None;
|
// return None;
|
||||||
}
|
// }
|
||||||
|
|
||||||
// let nav_model = self.nav_model()?;
|
// let nav_model = self.nav_model()?;
|
||||||
|
|
||||||
|
@ -271,89 +306,52 @@ impl App {
|
||||||
// .width(Length::Shrink)
|
// .width(Length::Shrink)
|
||||||
// .height(Length::Shrink);
|
// .height(Length::Shrink);
|
||||||
|
|
||||||
let list = self
|
let list =
|
||||||
.service
|
self.service.iter().enumerate().map(|(index, item)| {
|
||||||
.iter()
|
let icon = match item.kind {
|
||||||
.enumerate()
|
|
||||||
.map(|(index, item)| {
|
|
||||||
let button = button(item.title.clone()
|
|
||||||
.leading_icon({
|
|
||||||
match item.kind {
|
|
||||||
core::kinds::ServiceItemKind::Song(_) => {
|
core::kinds::ServiceItemKind::Song(_) => {
|
||||||
icon::from_name("folder-music-symbolic")
|
icon::from_name("folder-music-symbolic")
|
||||||
},
|
}
|
||||||
core::kinds::ServiceItemKind::Video(_) => {
|
core::kinds::ServiceItemKind::Video(_) => {
|
||||||
icon::from_name("folder-videos-symbolic")
|
icon::from_name("folder-videos-symbolic")
|
||||||
},
|
}
|
||||||
core::kinds::ServiceItemKind::Image(_) => {
|
core::kinds::ServiceItemKind::Image(_) => {
|
||||||
icon::from_name("folder-pictures-symbolic")
|
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")
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}))
|
core::kinds::ServiceItemKind::Presentation(_) => {
|
||||||
.class(iced::theme::style::Button::HeaderBar)
|
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)
|
.padding(5)
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.on_press(iced::Action::App(Message::ChangeServiceItem(index)));
|
.on_press(Message::ChangeServiceItem(index));
|
||||||
let tooltip = tooltip(button,
|
let tooltip = tooltip(
|
||||||
text::body(item.kind.to_string()),
|
button,
|
||||||
TPosition::Right);
|
text(item.kind.to_string()),
|
||||||
dnd_destination(tooltip, vec!["application/service-item".into()])
|
TPosition::Right,
|
||||||
.data_received_for::<ServiceItem>( move |item| {
|
);
|
||||||
if let Some(item) = item {
|
mouse_area(tooltip)
|
||||||
iced::Action::App(Message::AddServiceItem(index, item))
|
.on_release(Message::AddServiceItemDrop(index))
|
||||||
} 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))
|
|
||||||
})
|
|
||||||
.into()
|
.into()
|
||||||
});
|
});
|
||||||
|
|
||||||
let end_index = self.service.len();
|
let end_index = self.service.len();
|
||||||
let column = column![
|
let column = column![
|
||||||
text::heading("Service List").center().width(280),
|
text("Service List").center().width(280),
|
||||||
column(list).spacing(10),
|
column(list).spacing(10),
|
||||||
dnd_destination(
|
mouse_area(vertical_space(),).on_release(
|
||||||
vertical_space(),
|
Message::AppendServiceItem(
|
||||||
vec!["application/service-item".into()]
|
self.library_dragged_item.clone()
|
||||||
)
|
)
|
||||||
.data_received_for::<ServiceItem>(|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,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
.padding(10)
|
.padding(10)
|
||||||
|
@ -361,52 +359,51 @@ impl App {
|
||||||
let padding = Padding::new(0.0).top(20);
|
let padding = Padding::new(0.0).top(20);
|
||||||
let mut container = Container::new(column)
|
let mut container = Container::new(column)
|
||||||
// .height(Length::Fill)
|
// .height(Length::Fill)
|
||||||
.style(nav_bar_style)
|
// .style(nav_bar_style)
|
||||||
.padding(padding);
|
.padding(padding);
|
||||||
|
|
||||||
if !self.core().is_condensed() {
|
// if !self.core().is_condensed() {
|
||||||
container = container.max_width(280);
|
// container = container.max_width(280);
|
||||||
}
|
// }
|
||||||
Some(container.into())
|
Some(container.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn header_start(&self) -> Vec<Element<Self::Message>> {
|
fn header_start(&self) -> Vec<Element<Message>> {
|
||||||
vec![]
|
vec![]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn header_center(&self) -> Vec<Element<Self::Message>> {
|
fn header_center(&self) -> Vec<Element<Message>> {
|
||||||
vec![search_input("Search...", "")
|
vec![text_input("Search...", "")
|
||||||
.on_input(|_| Message::None)
|
.on_input(|_| Message::None)
|
||||||
.on_submit(|_| Message::None)
|
.on_submit(Message::None)
|
||||||
.on_focus(Message::SearchFocus)
|
|
||||||
.width(1200)
|
.width(1200)
|
||||||
.into()]
|
.into()]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn header_end(&self) -> Vec<Element<Self::Message>> {
|
fn header_end(&self) -> Vec<Element<Message>> {
|
||||||
// let editor_toggle = toggler(self.editor_mode.is_some())
|
// let editor_toggle = toggler(self.editor_mode.is_some())
|
||||||
// .label("Editor")
|
// .label("Editor")
|
||||||
// .spacing(10)
|
// .spacing(10)
|
||||||
// .width(Length::Shrink)
|
// .width(Length::Shrink)
|
||||||
// .on_toggle(Message::EditorToggle);
|
// .on_toggle(Message::EditorToggle);
|
||||||
|
|
||||||
let presenter_window = self.windows.get(1);
|
let presenter_window = self.windows.len() > 1;
|
||||||
let text = if self.presentation_open {
|
let presentation_button_text = if self.presentation_open {
|
||||||
text::body("End Presentation")
|
text("End Presentation")
|
||||||
} else {
|
} else {
|
||||||
text::body("Present")
|
text("Present")
|
||||||
};
|
};
|
||||||
|
|
||||||
let row = row![
|
let row = row![
|
||||||
tooltip(
|
tooltip(
|
||||||
button::custom(
|
button(
|
||||||
row!(
|
row!(
|
||||||
Container::new(
|
Container::new(
|
||||||
icon::from_name("document-edit-symbolic")
|
icon::from_name("document-edit-symbolic")
|
||||||
.scale(3)
|
.scale(3)
|
||||||
)
|
)
|
||||||
.center_y(Length::Fill),
|
.center_y(Length::Fill),
|
||||||
text::body(if self.editor_mode.is_some() {
|
text(if self.editor_mode.is_some() {
|
||||||
"Present Mode"
|
"Present Mode"
|
||||||
} else {
|
} else {
|
||||||
"Edit Mode"
|
"Edit Mode"
|
||||||
|
@ -414,7 +411,6 @@ impl App {
|
||||||
)
|
)
|
||||||
.spacing(5),
|
.spacing(5),
|
||||||
)
|
)
|
||||||
.class(iced::theme::style::Button::HeaderBar)
|
|
||||||
.on_press(Message::EditorToggle(
|
.on_press(Message::EditorToggle(
|
||||||
self.editor_mode.is_none(),
|
self.editor_mode.is_none(),
|
||||||
)),
|
)),
|
||||||
|
@ -422,7 +418,7 @@ impl App {
|
||||||
TPosition::Bottom,
|
TPosition::Bottom,
|
||||||
),
|
),
|
||||||
tooltip(
|
tooltip(
|
||||||
button::custom(
|
button(
|
||||||
row!(
|
row!(
|
||||||
Container::new(
|
Container::new(
|
||||||
icon::from_name(
|
icon::from_name(
|
||||||
|
@ -435,16 +431,16 @@ impl App {
|
||||||
.scale(3)
|
.scale(3)
|
||||||
)
|
)
|
||||||
.center_y(Length::Fill),
|
.center_y(Length::Fill),
|
||||||
text
|
presentation_button_text
|
||||||
)
|
)
|
||||||
.spacing(5),
|
.spacing(5),
|
||||||
)
|
)
|
||||||
.class(iced::theme::style::Button::HeaderBar)
|
|
||||||
.on_press({
|
.on_press({
|
||||||
if self.presentation_open {
|
if self.presentation_open {
|
||||||
Message::CloseWindow(
|
// Message::CloseWindow(
|
||||||
presenter_window.copied(),
|
// presenter_window.copied(),
|
||||||
)
|
// )
|
||||||
|
Message::None
|
||||||
} else {
|
} else {
|
||||||
Message::OpenWindow
|
Message::OpenWindow
|
||||||
}
|
}
|
||||||
|
@ -453,14 +449,14 @@ impl App {
|
||||||
TPosition::Bottom,
|
TPosition::Bottom,
|
||||||
),
|
),
|
||||||
tooltip(
|
tooltip(
|
||||||
button::custom(
|
button(
|
||||||
row!(
|
row!(
|
||||||
Container::new(
|
Container::new(
|
||||||
icon::from_name("view-list-symbolic")
|
icon::from_name("view-list-symbolic")
|
||||||
.scale(3)
|
.scale(3)
|
||||||
)
|
)
|
||||||
.center_y(Length::Fill),
|
.center_y(Length::Fill),
|
||||||
text::body(if self.library_open {
|
text(if self.library_open {
|
||||||
"Close Library"
|
"Close Library"
|
||||||
} else {
|
} else {
|
||||||
"Open Library"
|
"Open Library"
|
||||||
|
@ -468,7 +464,6 @@ impl App {
|
||||||
)
|
)
|
||||||
.spacing(5),
|
.spacing(5),
|
||||||
)
|
)
|
||||||
.class(iced::theme::style::Button::HeaderBar)
|
|
||||||
.on_press(Message::LibraryToggle),
|
.on_press(Message::LibraryToggle),
|
||||||
"Open Library",
|
"Open Library",
|
||||||
TPosition::Bottom,
|
TPosition::Bottom,
|
||||||
|
@ -479,25 +474,23 @@ impl App {
|
||||||
vec![row]
|
vec![row]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn footer(&self) -> Option<Element<Self::Message>> {
|
fn footer(&self) -> Option<Element<Message>> {
|
||||||
let total_items_text =
|
let total_items_text =
|
||||||
format!("Total Service Items: {}", self.service.len());
|
format!("Total Service Items: {}", self.service.len());
|
||||||
let total_slides_text =
|
let total_slides_text =
|
||||||
format!("Total Slides: {}", self.presenter.total_slides);
|
format!("Total Slides: {}", self.presenter.total_slides);
|
||||||
let row = row![
|
let row =
|
||||||
text::body(total_items_text),
|
row![text(total_items_text), text(total_slides_text)]
|
||||||
text::body(total_slides_text)
|
|
||||||
]
|
|
||||||
.spacing(10);
|
.spacing(10);
|
||||||
Some(
|
Some(
|
||||||
Container::new(row)
|
Container::new(row)
|
||||||
.align_right(Length::Fill)
|
.align_right(Length::Fill)
|
||||||
.padding([5, 0, 0, 0])
|
.padding([5, 0])
|
||||||
.into(),
|
.into(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn subscription(&self) -> Subscription<Self::Message> {
|
fn subscription(&self) -> Subscription<Message> {
|
||||||
event::listen_with(|event, _, id| {
|
event::listen_with(|event, _, id| {
|
||||||
// debug!(?event);
|
// debug!(?event);
|
||||||
match event {
|
match event {
|
||||||
|
@ -530,31 +523,12 @@ impl App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
iced::Event::Touch(_touch) => None,
|
iced::Event::Touch(_touch) => None,
|
||||||
iced::Event::A11y(_id, _action_request) => None,
|
iced::Event::InputMethod(event) => todo!(),
|
||||||
iced::Event::Dnd(_dnd_event) => None,
|
|
||||||
iced::Event::PlatformSpecific(_platform_specific) => {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn context_drawer(
|
fn dialog(&self) -> Option<Element<'_, Message>> {
|
||||||
&self,
|
|
||||||
) -> Option<iced::app::context_drawer::ContextDrawer<Self::Message>>
|
|
||||||
{
|
|
||||||
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<Element<'_, Self::Message>> {
|
|
||||||
if self.searching {
|
if self.searching {
|
||||||
Some(text("hello").into())
|
Some(text("hello").into())
|
||||||
} else {
|
} else {
|
||||||
|
@ -571,9 +545,7 @@ impl App {
|
||||||
// debug!(?message);
|
// debug!(?message);
|
||||||
match self.song_editor.update(message) {
|
match self.song_editor.update(message) {
|
||||||
song_editor::Action::Task(task) => {
|
song_editor::Action::Task(task) => {
|
||||||
task.map(|m| {
|
task.map(|m| Message::SongEditor(m))
|
||||||
iced::Action::App(Message::SongEditor(m))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
song_editor::Action::UpdateSong(song) => {
|
song_editor::Action::UpdateSong(song) => {
|
||||||
if let Some(library) = &mut self.library {
|
if let Some(library) = &mut self.library {
|
||||||
|
@ -595,10 +567,9 @@ impl App {
|
||||||
video.set_muted(false);
|
video.set_muted(false);
|
||||||
}
|
}
|
||||||
match self.presenter.update(message) {
|
match self.presenter.update(message) {
|
||||||
presenter::Action::Task(task) => task.map(|m| {
|
presenter::Action::Task(task) => {
|
||||||
// debug!("Should run future");
|
task.map(|m| Message::Present(m))
|
||||||
iced::Action::App(Message::Present(m))
|
}
|
||||||
}),
|
|
||||||
presenter::Action::None => Task::none(),
|
presenter::Action::None => Task::none(),
|
||||||
presenter::Action::NextSlide => {
|
presenter::Action::NextSlide => {
|
||||||
let slide_index = self.current_item.1;
|
let slide_index = self.current_item.1;
|
||||||
|
@ -626,9 +597,7 @@ impl App {
|
||||||
match action {
|
match action {
|
||||||
presenter::Action::Task(task) => {
|
presenter::Action::Task(task) => {
|
||||||
tasks.push(task.map(|m| {
|
tasks.push(task.map(|m| {
|
||||||
iced::Action::App(
|
Message::Present(m)
|
||||||
Message::Present(m),
|
|
||||||
)
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
_ => todo!(),
|
_ => todo!(),
|
||||||
|
@ -648,13 +617,7 @@ impl App {
|
||||||
presenter::Action::Task(
|
presenter::Action::Task(
|
||||||
task,
|
task,
|
||||||
) => tasks.push(task.map(
|
) => tasks.push(task.map(
|
||||||
|m| {
|
|m| Message::Present(m),
|
||||||
iced::Action::App(
|
|
||||||
Message::Present(
|
|
||||||
m,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)),
|
)),
|
||||||
_ => todo!(),
|
_ => todo!(),
|
||||||
}
|
}
|
||||||
|
@ -684,9 +647,7 @@ impl App {
|
||||||
match action {
|
match action {
|
||||||
presenter::Action::Task(task) => {
|
presenter::Action::Task(task) => {
|
||||||
tasks.push(task.map(|m| {
|
tasks.push(task.map(|m| {
|
||||||
iced::Action::App(
|
Message::Present(m)
|
||||||
Message::Present(m),
|
|
||||||
)
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
_ => todo!(),
|
_ => todo!(),
|
||||||
|
@ -721,13 +682,7 @@ impl App {
|
||||||
presenter::Action::Task(
|
presenter::Action::Task(
|
||||||
task,
|
task,
|
||||||
) => tasks.push(task.map(
|
) => tasks.push(task.map(
|
||||||
|m| {
|
|m| Message::Present(m),
|
||||||
iced::Action::App(
|
|
||||||
Message::Present(
|
|
||||||
m,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)),
|
)),
|
||||||
_ => todo!(),
|
_ => todo!(),
|
||||||
}
|
}
|
||||||
|
@ -749,9 +704,7 @@ impl App {
|
||||||
}
|
}
|
||||||
library::Action::Task(task) => {
|
library::Action::Task(task) => {
|
||||||
return task.map(|message| {
|
return task.map(|message| {
|
||||||
iced::Action::App(Message::Library(
|
Message::Library(message)
|
||||||
message,
|
|
||||||
))
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
library::Action::None => return Task::none(),
|
library::Action::None => return Task::none(),
|
||||||
|
@ -796,27 +749,7 @@ impl App {
|
||||||
self.file = file;
|
self.file = file;
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::OpenWindow => {
|
Message::OpenWindow => self.show_window(),
|
||||||
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::CloseWindow(id) => {
|
Message::CloseWindow(id) => {
|
||||||
if let Some(id) = id {
|
if let Some(id) = id {
|
||||||
window::close(id)
|
window::close(id)
|
||||||
|
@ -826,27 +759,21 @@ impl App {
|
||||||
}
|
}
|
||||||
Message::WindowOpened(id, _) => {
|
Message::WindowOpened(id, _) => {
|
||||||
debug!(?id, "Window opened");
|
debug!(?id, "Window opened");
|
||||||
if self.cli_mode
|
let main_id =
|
||||||
|| id > self.core.main_window_id().expect("Iced core seems to be missing a main window, was this started in cli mode?")
|
self.windows.first_key_value().unwrap().0;
|
||||||
{
|
if self.cli_mode || &id > main_id {
|
||||||
self.presentation_open = true;
|
self.presentation_open = true;
|
||||||
if let Some(video) = &mut self.presenter.video {
|
if let Some(video) = &mut self.presenter.video {
|
||||||
video.set_muted(false);
|
video.set_muted(false);
|
||||||
}
|
}
|
||||||
window::change_mode(id, Mode::Fullscreen)
|
window::set_mode(id, Mode::Fullscreen)
|
||||||
} else {
|
} else {
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::WindowClosed(id) => {
|
Message::WindowClosed(id) => {
|
||||||
warn!("Closing window: {id}");
|
warn!("Closing window: {id}");
|
||||||
let Some(window) =
|
self.windows.remove(&id);
|
||||||
self.windows.iter().position(|w| *w == id)
|
|
||||||
else {
|
|
||||||
error!("Nothing matches this window id: {id}");
|
|
||||||
return Task::none();
|
|
||||||
};
|
|
||||||
self.windows.remove(window);
|
|
||||||
// This closes the app if using the cli example
|
// This closes the app if using the cli example
|
||||||
if self.windows.is_empty() {
|
if self.windows.is_empty() {
|
||||||
self.update(Message::Quit)
|
self.update(Message::Quit)
|
||||||
|
@ -862,9 +789,8 @@ impl App {
|
||||||
self.library_open = !self.library_open;
|
self.library_open = !self.library_open;
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::Quit => iced::iced::exit(),
|
Message::Quit => iced::exit(),
|
||||||
Message::DndEnter(entity, data) => {
|
Message::DndEnter(data) => {
|
||||||
debug!(?entity);
|
|
||||||
debug!(?data);
|
debug!(?data);
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
@ -884,10 +810,6 @@ impl App {
|
||||||
// }
|
// }
|
||||||
let item = library.get_song(item).unwrap();
|
let item = library.get_song(item).unwrap();
|
||||||
let item = ServiceItem::from(item);
|
let item = ServiceItem::from(item);
|
||||||
self.nav_model
|
|
||||||
.insert()
|
|
||||||
.text(item.title.clone())
|
|
||||||
.data(item);
|
|
||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
@ -896,7 +818,7 @@ impl App {
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::None => Task::none(),
|
Message::None => Task::none(),
|
||||||
Message::DndLeave(entity) => {
|
Message::DndLeave() => {
|
||||||
// debug!(?entity);
|
// debug!(?entity);
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
@ -940,14 +862,24 @@ impl App {
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::AppendServiceItem(item) => {
|
Message::AppendServiceItem(item) => {
|
||||||
|
if let Some(item) = item {
|
||||||
self.service.push(item);
|
self.service.push(item);
|
||||||
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main window view
|
// Main window view
|
||||||
fn view(&self) -> Element<Message> {
|
fn view(&self, window: window::Id) -> Element<Message> {
|
||||||
|
if window == *self.windows.first_key_value().unwrap().0 {
|
||||||
|
self.main_view()
|
||||||
|
} else {
|
||||||
|
self.view_presenter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main_view(&self) -> Element<Message> {
|
||||||
let icon_left = icon::from_name("arrow-left");
|
let icon_left = icon::from_name("arrow-left");
|
||||||
let icon_right = icon::from_name("arrow-right");
|
let icon_right = icon::from_name("arrow-right");
|
||||||
|
|
||||||
|
@ -956,24 +888,29 @@ impl App {
|
||||||
|video| video.duration().as_secs_f32(),
|
|video| video.duration().as_secs_f32(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let video_button_icon =
|
let video_button_icon = if let Some(video) =
|
||||||
if let Some(video) = &self.presenter.video {
|
&self.presenter.video
|
||||||
let (icon_name, tooltip) = if video.paused() {
|
{
|
||||||
|
let (icon_name, tt) = if video.paused() {
|
||||||
("media-play", "Play")
|
("media-play", "Play")
|
||||||
} else {
|
} else {
|
||||||
("media-pause", "Pause")
|
("media-pause", "Pause")
|
||||||
};
|
};
|
||||||
button::icon(icon::from_name(icon_name))
|
tooltip(
|
||||||
.tooltip(tooltip)
|
button(icon::from_name(icon_name)).on_press(
|
||||||
.on_press(Message::Present(
|
Message::Present(presenter::Message::StartVideo),
|
||||||
presenter::Message::StartVideo,
|
),
|
||||||
))
|
tt,
|
||||||
|
TPosition::FollowCursor,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
button::icon(icon::from_name("media-play"))
|
tooltip(
|
||||||
.tooltip("Play")
|
button(icon::from_name("media-play")).on_press(
|
||||||
.on_press(Message::Present(
|
Message::Present(presenter::Message::StartVideo),
|
||||||
presenter::Message::StartVideo,
|
),
|
||||||
))
|
"Play",
|
||||||
|
TPosition::FollowCursor,
|
||||||
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
let slide_preview = column![
|
let slide_preview = column![
|
||||||
|
@ -999,7 +936,7 @@ impl App {
|
||||||
.step(0.1)
|
.step(0.1)
|
||||||
)
|
)
|
||||||
.center_x(Length::Fill)
|
.center_x(Length::Fill)
|
||||||
.padding([7, 0, 0, 0])
|
.padding([7, 0])
|
||||||
]
|
]
|
||||||
.padding(5)
|
.padding(5)
|
||||||
} else {
|
} else {
|
||||||
|
@ -1016,7 +953,7 @@ impl App {
|
||||||
} else {
|
} else {
|
||||||
Space::new(0, 0).into()
|
Space::new(0, 0).into()
|
||||||
})
|
})
|
||||||
.style(nav_bar_style)
|
// .style(nav_bar_style)
|
||||||
.center(Length::FillPortion(2))
|
.center(Length::FillPortion(2))
|
||||||
} else {
|
} else {
|
||||||
Container::new(horizontal_space().width(0))
|
Container::new(horizontal_space().width(0))
|
||||||
|
@ -1026,31 +963,25 @@ impl App {
|
||||||
self.song_editor.view().map(Message::SongEditor);
|
self.song_editor.view().map(Message::SongEditor);
|
||||||
|
|
||||||
let row = row![
|
let row = row![
|
||||||
Container::new(
|
Container::new(tooltip(
|
||||||
button::icon(icon_left)
|
button(icon_left.size(128)).width(128).on_press(
|
||||||
.icon_size(128)
|
Message::Present(presenter::Message::PrevSlide)
|
||||||
.tooltip("Previous Slide")
|
),
|
||||||
.width(128)
|
"Previous Slide",
|
||||||
.on_press(Message::Present(
|
TPosition::FollowCursor
|
||||||
presenter::Message::PrevSlide
|
|
||||||
))
|
))
|
||||||
.class(theme::style::Button::Transparent)
|
|
||||||
)
|
|
||||||
.center_y(Length::Fill)
|
.center_y(Length::Fill)
|
||||||
.align_right(Length::FillPortion(1)),
|
.align_right(Length::FillPortion(1)),
|
||||||
Container::new(slide_preview)
|
Container::new(slide_preview)
|
||||||
.center_y(Length::Fill)
|
.center_y(Length::Fill)
|
||||||
.width(Length::FillPortion(3)),
|
.width(Length::FillPortion(3)),
|
||||||
Container::new(
|
Container::new(tooltip(
|
||||||
button::icon(icon_right)
|
button(icon_right.size(128)).width(128).on_press(
|
||||||
.icon_size(128)
|
Message::Present(presenter::Message::NextSlide)
|
||||||
.tooltip("Next Slide")
|
),
|
||||||
.width(128)
|
"Next Slide",
|
||||||
.on_press(Message::Present(
|
TPosition::FollowCursor
|
||||||
presenter::Message::NextSlide
|
|
||||||
))
|
))
|
||||||
.class(theme::style::Button::Transparent)
|
|
||||||
)
|
|
||||||
.center_y(Length::Fill)
|
.center_y(Length::Fill)
|
||||||
.align_left(Length::FillPortion(1)),
|
.align_left(Length::FillPortion(1)),
|
||||||
library
|
library
|
||||||
|
@ -1077,49 +1008,38 @@ impl App {
|
||||||
}
|
}
|
||||||
|
|
||||||
// View for presentation
|
// View for presentation
|
||||||
fn view_window(&self, _id: window::Id) -> Element<Message> {
|
fn view_presenter(&self) -> Element<Message> {
|
||||||
self.presenter.view().map(Message::Present)
|
self.presenter.view().map(Message::Present)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl App
|
// fn update_title(&mut self) -> Task<Message> {
|
||||||
where
|
// let window_title = "Lumina";
|
||||||
Self: iced::Application,
|
// self.windows.first_key_value().unwrap().1.title =
|
||||||
{
|
// window_title.to_string();
|
||||||
fn active_page_title(&self) -> &str {
|
// Task::none()
|
||||||
let Some(label) =
|
// }
|
||||||
self.nav_model.text(self.nav_model.active())
|
|
||||||
else {
|
|
||||||
return "Lumina";
|
|
||||||
};
|
|
||||||
label
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_title(&mut self) -> Task<Message> {
|
|
||||||
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 show_window(&mut self) -> Task<Message> {
|
fn show_window(&mut self) -> Task<Message> {
|
||||||
let (id, spawn_window) = window::open(window::Settings {
|
let (id, spawn_window) = window::open(window::Settings {
|
||||||
position: Position::Centered,
|
position: Position::Centered,
|
||||||
exit_on_close_request: true,
|
exit_on_close_request: true,
|
||||||
decorations: false,
|
decorations: false,
|
||||||
|
fullscreen: true,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
self.windows.push(id);
|
let window = Window {
|
||||||
_ = self.set_window_title("Lumina Presenter".to_owned(), id);
|
title: "Presentation".into(),
|
||||||
spawn_window.map(|id| {
|
scale_input: "".into(),
|
||||||
iced::Action::App(Message::WindowOpened(id, None))
|
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<Message> {
|
fn add_library(&mut self) -> Task<Message> {
|
||||||
Task::perform(async move { Library::new().await }, |x| {
|
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(
|
Theme::custom(
|
||||||
"Snazzy",
|
"Snazzy",
|
||||||
Palette {
|
Palette {
|
||||||
|
|
|
@ -4,17 +4,18 @@ use iced::{
|
||||||
futures::FutureExt,
|
futures::FutureExt,
|
||||||
theme,
|
theme,
|
||||||
widget::{
|
widget::{
|
||||||
button, container, horizontal_space, mouse_area, responsive,
|
button, column, container, horizontal_space, mouse_area,
|
||||||
row, scrollable, text, text_input, Container, Space,
|
responsive, row, scrollable, text, text_input, Container,
|
||||||
|
Space,
|
||||||
},
|
},
|
||||||
widget::{column, row as rowm, text as textm},
|
|
||||||
Background, Border, Color, Element, Length, Task,
|
Background, Border, Color, Element, Length, Task,
|
||||||
};
|
};
|
||||||
use miette::{IntoDiagnostic, Result};
|
use miette::{IntoDiagnostic, Result};
|
||||||
use sqlx::{pool::PoolConnection, Sqlite, SqlitePool};
|
use sqlx::{pool::PoolConnection, Sqlite, SqlitePool};
|
||||||
use tracing::{debug, error, warn};
|
use tracing::{debug, error, warn};
|
||||||
|
|
||||||
use crate::core::{
|
use crate::{
|
||||||
|
core::{
|
||||||
content::Content,
|
content::Content,
|
||||||
images::{update_image_in_db, Image},
|
images::{update_image_in_db, Image},
|
||||||
model::{LibraryKind, Model},
|
model::{LibraryKind, Model},
|
||||||
|
@ -22,6 +23,8 @@ use crate::core::{
|
||||||
service_items::ServiceItem,
|
service_items::ServiceItem,
|
||||||
songs::{update_song_in_db, Song},
|
songs::{update_song_in_db, Song},
|
||||||
videos::{update_video_in_db, Video},
|
videos::{update_video_in_db, Video},
|
||||||
|
},
|
||||||
|
ui::widgets::icon,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
@ -276,40 +279,40 @@ impl<'a> Library {
|
||||||
where
|
where
|
||||||
T: Content,
|
T: Content,
|
||||||
{
|
{
|
||||||
let mut row = row::<Message>().spacing(5);
|
let mut row = row![].spacing(5);
|
||||||
match &model.kind {
|
match &model.kind {
|
||||||
LibraryKind::Song => {
|
LibraryKind::Song => {
|
||||||
row = row
|
row = row
|
||||||
.push(icon::from_name("folder-music-symbolic"));
|
.push(icon::from_name("folder-music-symbolic"));
|
||||||
row = row
|
row =
|
||||||
.push(textm!("Songs").align_y(Vertical::Center));
|
row.push(text("Songs").align_y(Vertical::Center));
|
||||||
}
|
}
|
||||||
LibraryKind::Video => {
|
LibraryKind::Video => {
|
||||||
row = row
|
row = row
|
||||||
.push(icon::from_name("folder-videos-symbolic"));
|
.push(icon::from_name("folder-videos-symbolic"));
|
||||||
row = row
|
row = row
|
||||||
.push(textm!("Videos").align_y(Vertical::Center));
|
.push(text("Videos").align_y(Vertical::Center));
|
||||||
}
|
}
|
||||||
LibraryKind::Image => {
|
LibraryKind::Image => {
|
||||||
row = row.push(icon::from_name(
|
row = row.push(icon::from_name(
|
||||||
"folder-pictures-symbolic",
|
"folder-pictures-symbolic",
|
||||||
));
|
));
|
||||||
row = row
|
row = row
|
||||||
.push(textm!("Images").align_y(Vertical::Center));
|
.push(text("Images").align_y(Vertical::Center));
|
||||||
}
|
}
|
||||||
LibraryKind::Presentation => {
|
LibraryKind::Presentation => {
|
||||||
row = row.push(icon::from_name(
|
row = row.push(icon::from_name(
|
||||||
"x-office-presentation-symbolic",
|
"x-office-presentation-symbolic",
|
||||||
));
|
));
|
||||||
row = row.push(
|
row = row.push(
|
||||||
textm!("Presentations").align_y(Vertical::Center),
|
text("Presentations").align_y(Vertical::Center),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let item_count = model.items.len();
|
let item_count = model.items.len();
|
||||||
row = row.push(horizontal_space());
|
row = row.push(horizontal_space());
|
||||||
row = row
|
row = row
|
||||||
.push(textm!("{}", item_count).align_y(Vertical::Center));
|
.push(text!("{}", item_count).align_y(Vertical::Center));
|
||||||
row = row.push(
|
row = row.push(
|
||||||
icon::from_name({
|
icon::from_name({
|
||||||
if self.library_open == Some(model.kind) {
|
if self.library_open == Some(model.kind) {
|
||||||
|
@ -329,21 +332,26 @@ impl<'a> Library {
|
||||||
match self.library_hovered {
|
match self.library_hovered {
|
||||||
Some(lib) => Background::Color(
|
Some(lib) => Background::Color(
|
||||||
if lib == model.kind {
|
if lib == model.kind {
|
||||||
t.iced().button.hover.into()
|
t.extended_palette()
|
||||||
|
.primary
|
||||||
|
.strong
|
||||||
|
.color
|
||||||
} else {
|
} else {
|
||||||
t.iced().button.base.into()
|
t.extended_palette()
|
||||||
|
.background
|
||||||
|
.base
|
||||||
|
.color
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
None => Background::Color(
|
None => Background::Color(
|
||||||
t.iced().button.base.into(),
|
t.extended_palette()
|
||||||
|
.background
|
||||||
|
.base
|
||||||
|
.color,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.border(
|
.border(Border::default().rounded(5))
|
||||||
Border::default().rounded(
|
|
||||||
t.iced().corner_radii.radius_s,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
.center_x(Length::Fill)
|
.center_x(Length::Fill)
|
||||||
.center_y(Length::Shrink);
|
.center_y(Length::Shrink);
|
||||||
|
@ -366,52 +374,21 @@ impl<'a> Library {
|
||||||
let visual_item = self
|
let visual_item = self
|
||||||
.single_item(index, item, model)
|
.single_item(index, item, model)
|
||||||
.map(|_| Message::None);
|
.map(|_| Message::None);
|
||||||
DndSource::<Message, ServiceItem>::new(
|
|
||||||
mouse_area(visual_item)
|
mouse_area(visual_item)
|
||||||
.on_drag(Message::DragItem(service_item.clone()))
|
// .on_drag(Message::DragItem(
|
||||||
.on_enter(Message::HoverItem(
|
// service_item.clone(),
|
||||||
Some((
|
// ))
|
||||||
|
.on_enter(Message::HoverItem(Some((
|
||||||
model.kind,
|
model.kind,
|
||||||
index as i32,
|
index as i32,
|
||||||
)),
|
))))
|
||||||
|
.on_double_click(Message::OpenItem(
|
||||||
|
Some((model.kind, index as i32)),
|
||||||
))
|
))
|
||||||
.on_double_click(
|
|
||||||
Message::OpenItem(Some((
|
|
||||||
model.kind,
|
|
||||||
index as i32,
|
|
||||||
))),
|
|
||||||
)
|
|
||||||
.on_exit(Message::HoverItem(None))
|
.on_exit(Message::HoverItem(None))
|
||||||
.on_press(Message::SelectItem(
|
.on_press(Message::SelectItem(Some(
|
||||||
Some((
|
(model.kind, index as i32),
|
||||||
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()
|
.into()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -422,9 +399,9 @@ impl<'a> Library {
|
||||||
.spacing(5)
|
.spacing(5)
|
||||||
.height(Length::Fill);
|
.height(Length::Fill);
|
||||||
|
|
||||||
let library_toolbar = rowm!(
|
let library_toolbar = row!(
|
||||||
text_input("Search...", ""),
|
text_input("Search...", ""),
|
||||||
button::icon(icon::from_name("add"))
|
button(icon::from_name("add"))
|
||||||
);
|
);
|
||||||
let library_column =
|
let library_column =
|
||||||
column![library_toolbar, items].spacing(3);
|
column![library_toolbar, items].spacing(3);
|
||||||
|
@ -445,33 +422,36 @@ impl<'a> Library {
|
||||||
where
|
where
|
||||||
T: Content,
|
T: Content,
|
||||||
{
|
{
|
||||||
let text = Container::new(responsive(|size| {
|
let item_text = Container::new(responsive(|size| {
|
||||||
text::heading(elide_text(item.title(), size.width))
|
text(elide_text(item.title(), size.width))
|
||||||
.center()
|
.center()
|
||||||
.wrapping(textm::Wrapping::None)
|
.wrapping(text::Wrapping::None)
|
||||||
.into()
|
.into()
|
||||||
}))
|
}))
|
||||||
.center_y(20)
|
.center_y(20)
|
||||||
.center_x(Length::Fill);
|
.center_x(Length::Fill);
|
||||||
let subtext = container(responsive(|size| {
|
let subtext = container(responsive(|size| {
|
||||||
let color: Color = if item.background().is_some() {
|
if item.background().is_some() {
|
||||||
theme::active().iced().accent_text_color().into()
|
text(elide_text(item.subtext(), size.width))
|
||||||
} else {
|
.style(text::primary)
|
||||||
theme::active().iced().destructive_text_color().into()
|
|
||||||
};
|
|
||||||
text::body(elide_text(item.subtext(), size.width))
|
|
||||||
.center()
|
.center()
|
||||||
.wrapping(textm::Wrapping::None)
|
.wrapping(text::Wrapping::None)
|
||||||
.class(color)
|
|
||||||
.into()
|
.into()
|
||||||
|
} else {
|
||||||
|
text(elide_text(item.subtext(), size.width))
|
||||||
|
.style(text::primary)
|
||||||
|
.center()
|
||||||
|
.wrapping(text::Wrapping::None)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
}))
|
}))
|
||||||
.center_y(20)
|
.center_y(20)
|
||||||
.center_x(Length::Fill);
|
.center_x(Length::Fill);
|
||||||
|
|
||||||
let texts = column([text.into(), subtext.into()]);
|
let texts = column([item_text.into(), subtext.into()]);
|
||||||
|
|
||||||
Container::new(
|
Container::new(
|
||||||
rowm![horizontal_space().width(0), texts]
|
row![horizontal_space().width(0), texts]
|
||||||
.spacing(10)
|
.spacing(10)
|
||||||
.align_y(Vertical::Center),
|
.align_y(Vertical::Center),
|
||||||
)
|
)
|
||||||
|
@ -486,9 +466,9 @@ impl<'a> Library {
|
||||||
if model.kind == library
|
if model.kind == library
|
||||||
&& selected == index as i32
|
&& selected == index as i32
|
||||||
{
|
{
|
||||||
t.iced().accent.selected.into()
|
t.extended_palette().primary.strong.color
|
||||||
} else {
|
} else {
|
||||||
t.iced().button.base.into()
|
t.extended_palette().primary.base.color
|
||||||
}
|
}
|
||||||
} else if let Some((library, hovered)) =
|
} else if let Some((library, hovered)) =
|
||||||
self.hovered_item
|
self.hovered_item
|
||||||
|
@ -496,18 +476,15 @@ impl<'a> Library {
|
||||||
if model.kind == library
|
if model.kind == library
|
||||||
&& hovered == index as i32
|
&& hovered == index as i32
|
||||||
{
|
{
|
||||||
t.iced().button.hover.into()
|
t.extended_palette().primary.strong.color
|
||||||
} else {
|
} else {
|
||||||
t.iced().button.base.into()
|
t.extended_palette().primary.base.color
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
t.iced().button.base.into()
|
t.extended_palette().background.strong.color
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
.border(
|
.border(Border::default().rounded(10))
|
||||||
Border::default()
|
|
||||||
.rounded(t.iced().corner_radii.radius_m),
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,18 +6,15 @@ use iced::{
|
||||||
border,
|
border,
|
||||||
font::{Family, Stretch, Style, Weight},
|
font::{Family, Stretch, Style, Weight},
|
||||||
widget::{
|
widget::{
|
||||||
container, image, mouse_area, responsive, scrollable, text,
|
container, image, mouse_area, responsive, rich_text,
|
||||||
Column, Container, Row, Space,
|
|
||||||
},
|
|
||||||
widget::{
|
|
||||||
rich_text,
|
|
||||||
scrollable::{
|
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,
|
Background, Border, Color, ContentFit, Element, Font, Length,
|
||||||
Task, Vector,
|
Shadow, Task, Vector,
|
||||||
};
|
};
|
||||||
use iced_video_player::{Position, Video, VideoPlayer};
|
use iced_video_player::{Position, Video, VideoPlayer};
|
||||||
use rodio::{Decoder, OutputStream, Sink};
|
use rodio::{Decoder, OutputStream, Sink};
|
||||||
|
@ -164,7 +161,7 @@ impl Presenter {
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
scroll_id: Id::unique(),
|
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(
|
return Action::Task(Task::perform(
|
||||||
async move {
|
async move {
|
||||||
tokio::task::spawn_blocking(move || {
|
tokio::task::spawn_blocking(move || {
|
||||||
match gst_pbutils::MissingPluginMessage::parse(&element) {
|
// match gst_pbutils::MissingPluginMessage::parse(&element) {
|
||||||
Ok(missing_plugin) => {
|
// Ok(missing_plugin) => {
|
||||||
let mut install_ctx = gst_pbutils::InstallPluginsContext::new();
|
// let mut install_ctx = gst_pbutils::InstallPluginsContext::new();
|
||||||
install_ctx
|
// install_ctx
|
||||||
.set_desktop_id(&format!("{}.desktop", "org.chriscochrun.lumina"));
|
// .set_desktop_id(&format!("{}.desktop", "org.chriscochrun.lumina"));
|
||||||
let install_detail = missing_plugin.installer_detail();
|
// let install_detail = missing_plugin.installer_detail();
|
||||||
println!("installing plugins: {}", install_detail);
|
// println!("installing plugins: {}", install_detail);
|
||||||
let status = gst_pbutils::missing_plugins::install_plugins_sync(
|
// let status = gst_pbutils::missing_plugins::install_plugins_sync(
|
||||||
&[&install_detail],
|
// &[&install_detail],
|
||||||
Some(&install_ctx),
|
// Some(&install_ctx),
|
||||||
);
|
// );
|
||||||
info!("plugin install status: {}", status);
|
// info!("plugin install status: {}", status);
|
||||||
info!(
|
// info!(
|
||||||
"gstreamer registry update: {:?}",
|
// "gstreamer registry update: {:?}",
|
||||||
gstreamer::Registry::update()
|
// gstreamer::Registry::update()
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
Err(err) => {
|
// Err(err) => {
|
||||||
warn!("failed to parse missing plugin message: {err}");
|
// warn!("failed to parse missing plugin message: {err}");
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
Message::None
|
Message::None
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
|
@ -445,7 +442,7 @@ impl Presenter {
|
||||||
.style(move |t| {
|
.style(move |t| {
|
||||||
let mut style =
|
let mut style =
|
||||||
container::Style::default();
|
container::Style::default();
|
||||||
let theme = t.iced();
|
let theme = t;
|
||||||
let hovered = self.hovered_slide
|
let hovered = self.hovered_slide
|
||||||
== Some((
|
== Some((
|
||||||
item_index,
|
item_index,
|
||||||
|
@ -455,19 +452,25 @@ impl Presenter {
|
||||||
Some(Background::Color(
|
Some(Background::Color(
|
||||||
if is_current_slide {
|
if is_current_slide {
|
||||||
theme
|
theme
|
||||||
.accent
|
.extended_palette(
|
||||||
.base
|
)
|
||||||
.into()
|
.secondary
|
||||||
|
.strong
|
||||||
|
.color
|
||||||
} else if hovered {
|
} else if hovered {
|
||||||
theme
|
theme
|
||||||
.accent
|
.extended_palette(
|
||||||
.hover
|
)
|
||||||
.into()
|
.secondary
|
||||||
|
.strong
|
||||||
|
.color
|
||||||
} else {
|
} else {
|
||||||
theme
|
theme
|
||||||
.palette
|
.extended_palette(
|
||||||
.neutral_3
|
)
|
||||||
.into()
|
.background
|
||||||
|
.neutral
|
||||||
|
.color
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
style.border = Border::default()
|
style.border = Border::default()
|
||||||
|
@ -500,7 +503,7 @@ impl Presenter {
|
||||||
.padding(10),
|
.padding(10),
|
||||||
)
|
)
|
||||||
.interaction(
|
.interaction(
|
||||||
iced::iced::mouse::Interaction::Pointer,
|
iced::mouse::Interaction::Pointer,
|
||||||
)
|
)
|
||||||
.on_move(move |_| {
|
.on_move(move |_| {
|
||||||
Message::HoveredSlide(Some((
|
Message::HoveredSlide(Some((
|
||||||
|
@ -518,11 +521,11 @@ impl Presenter {
|
||||||
let row = Row::from_vec(slides)
|
let row = Row::from_vec(slides)
|
||||||
.spacing(10)
|
.spacing(10)
|
||||||
.padding([20, 15]);
|
.padding([20, 15]);
|
||||||
let label = text::body(item.title.clone());
|
let label = text(item.title.clone());
|
||||||
let label_container = container(label)
|
let label_container = container(label)
|
||||||
.align_top(Length::Fill)
|
.align_top(Length::Fill)
|
||||||
.align_left(Length::Fill)
|
.align_left(Length::Fill)
|
||||||
.padding([0, 0, 0, 35]);
|
.padding([0, 35]);
|
||||||
let divider = vertical_rule(2);
|
let divider = vertical_rule(2);
|
||||||
items.push(
|
items.push(
|
||||||
container(stack!(row, label_container))
|
container(stack!(row, label_container))
|
||||||
|
@ -532,11 +535,12 @@ impl Presenter {
|
||||||
items.push(divider.into());
|
items.push(divider.into());
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
let row =
|
let row = scrollable::Scrollable::new(
|
||||||
scrollable(container(Row::from_vec(items)).style(|t| {
|
container(Row::from_vec(items)).style(|t| {
|
||||||
let style = container::Style::default();
|
let style = container::Style::default();
|
||||||
style.border(Border::default().width(2))
|
style.border(Border::default().width(2))
|
||||||
}))
|
}),
|
||||||
|
)
|
||||||
.direction(Direction::Horizontal(Scrollbar::new()))
|
.direction(Direction::Horizontal(Scrollbar::new()))
|
||||||
.height(Length::Fill)
|
.height(Length::Fill)
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
|
@ -819,15 +823,15 @@ pub(crate) fn slide_view(
|
||||||
} else if let Some(video) = &video {
|
} else if let Some(video) = &video {
|
||||||
Container::new(
|
Container::new(
|
||||||
VideoPlayer::new(video)
|
VideoPlayer::new(video)
|
||||||
.mouse_hidden(hide_mouse)
|
// .mouse_hidden(hide_mouse)
|
||||||
.width(width)
|
.width(width)
|
||||||
.height(size.height)
|
.height(size.height)
|
||||||
.on_end_of_stream(Message::EndVideo)
|
.on_end_of_stream(Message::EndVideo)
|
||||||
.on_new_frame(Message::VideoFrame)
|
.on_new_frame(Message::VideoFrame)
|
||||||
.on_missing_plugin(Message::MissingPlugin)
|
// .on_missing_plugin(Message::MissingPlugin)
|
||||||
.on_warning(|w| {
|
// .on_warning(|w| {
|
||||||
Message::Error(w.to_string())
|
// Message::Error(w.to_string())
|
||||||
})
|
// })
|
||||||
.on_error(|e| {
|
.on_error(|e| {
|
||||||
Message::Error(e.to_string())
|
Message::Error(e.to_string())
|
||||||
})
|
})
|
||||||
|
|
|
@ -49,7 +49,7 @@ pub enum SlideError {
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
struct EditorProgram {
|
struct EditorProgram {
|
||||||
mouse_button_pressed: Option<iced::iced::mouse::Button>,
|
mouse_button_pressed: Option<iced::mouse::Button>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SlideEditor {
|
impl SlideEditor {
|
||||||
|
@ -78,8 +78,8 @@ impl<'a> Program<SlideWidget, iced::Theme, iced::Renderer>
|
||||||
state: &Self::State,
|
state: &Self::State,
|
||||||
renderer: &Renderer,
|
renderer: &Renderer,
|
||||||
theme: &iced::Theme,
|
theme: &iced::Theme,
|
||||||
bounds: iced::iced::Rectangle,
|
bounds: iced::Rectangle,
|
||||||
cursor: iced::iced_core::mouse::Cursor,
|
cursor: iced::mouse::Cursor,
|
||||||
) -> Vec<canvas::Geometry<Renderer>> {
|
) -> Vec<canvas::Geometry<Renderer>> {
|
||||||
// We prepare a new `Frame`
|
// We prepare a new `Frame`
|
||||||
let mut frame = canvas::Frame::new(renderer, bounds.size());
|
let mut frame = canvas::Frame::new(renderer, bounds.size());
|
||||||
|
@ -88,7 +88,7 @@ impl<'a> Program<SlideWidget, iced::Theme, iced::Renderer>
|
||||||
// We create a `Path` representing a simple circle
|
// We create a `Path` representing a simple circle
|
||||||
let circle = canvas::Path::circle(frame.center(), 50.0);
|
let circle = canvas::Path::circle(frame.center(), 50.0);
|
||||||
let border = canvas::Path::rectangle(
|
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),
|
Size::new(frame_rect.width, frame_rect.height),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -114,21 +114,19 @@ impl<'a> Program<SlideWidget, iced::Theme, iced::Renderer>
|
||||||
fn update(
|
fn update(
|
||||||
&self,
|
&self,
|
||||||
_state: &mut Self::State,
|
_state: &mut Self::State,
|
||||||
event: canvas::Event,
|
event: &iced::Event,
|
||||||
bounds: iced::iced::Rectangle,
|
bounds: iced::Rectangle,
|
||||||
_cursor: iced::iced_core::mouse::Cursor,
|
_cursor: iced::mouse::Cursor,
|
||||||
) -> (canvas::event::Status, Option<SlideWidget>) {
|
) -> std::option::Option<iced::widget::Action<SlideWidget>> {
|
||||||
match event {
|
match event {
|
||||||
canvas::Event::Mouse(event) => match event {
|
iced::Event::Mouse(event) => match event {
|
||||||
iced::iced::mouse::Event::CursorEntered => {
|
iced::mouse::Event::CursorEntered => {
|
||||||
debug!("cursor entered")
|
debug!("cursor entered")
|
||||||
}
|
}
|
||||||
iced::iced::mouse::Event::CursorLeft => {
|
iced::mouse::Event::CursorLeft => {
|
||||||
debug!("cursor left")
|
debug!("cursor left")
|
||||||
}
|
}
|
||||||
iced::iced::mouse::Event::CursorMoved {
|
iced::mouse::Event::CursorMoved { position } => {
|
||||||
position,
|
|
||||||
} => {
|
|
||||||
if bounds.x < position.x
|
if bounds.x < position.x
|
||||||
&& bounds.y < position.y
|
&& bounds.y < position.y
|
||||||
&& (bounds.width + bounds.x) > position.x
|
&& (bounds.width + bounds.x) > position.x
|
||||||
|
@ -137,29 +135,34 @@ impl<'a> Program<SlideWidget, iced::Theme, iced::Renderer>
|
||||||
debug!(?position, "cursor moved");
|
debug!(?position, "cursor moved");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
iced::iced::mouse::Event::ButtonPressed(button) => {
|
iced::mouse::Event::ButtonPressed(button) => {
|
||||||
// self.mouse_button_pressed = Some(button);
|
// self.mouse_button_pressed = Some(button);
|
||||||
debug!(?button, "mouse button pressed")
|
debug!(?button, "mouse button pressed")
|
||||||
}
|
}
|
||||||
iced::iced::mouse::Event::ButtonReleased(button) => {
|
iced::mouse::Event::ButtonReleased(button) => {
|
||||||
debug!(?button, "mouse button released")
|
debug!(?button, "mouse button released")
|
||||||
}
|
}
|
||||||
iced::iced::mouse::Event::WheelScrolled { delta } => {
|
iced::mouse::Event::WheelScrolled { delta } => {
|
||||||
debug!(?delta, "scroll wheel")
|
debug!(?delta, "scroll wheel")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
canvas::Event::Touch(event) => debug!("test"),
|
iced::Event::Touch(event) => debug!("test"),
|
||||||
canvas::Event::Keyboard(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(
|
fn mouse_interaction(
|
||||||
&self,
|
&self,
|
||||||
_state: &Self::State,
|
_state: &Self::State,
|
||||||
_bounds: iced::iced::Rectangle,
|
_bounds: iced::Rectangle,
|
||||||
_cursor: iced::iced_core::mouse::Cursor,
|
_cursor: iced::mouse::Cursor,
|
||||||
) -> iced::iced_core::mouse::Interaction {
|
) -> iced::mouse::Interaction {
|
||||||
iced::iced_core::mouse::Interaction::default()
|
iced::mouse::Interaction::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,19 +5,22 @@ use iced::{
|
||||||
advanced::graphics::text::cosmic_text::fontdb,
|
advanced::graphics::text::cosmic_text::fontdb,
|
||||||
font::{Family, Stretch, Style, Weight},
|
font::{Family, Stretch, Style, Weight},
|
||||||
theme,
|
theme,
|
||||||
widget::row,
|
|
||||||
widget::{
|
widget::{
|
||||||
button, column, combo_box, container, horizontal_space,
|
button, column, combo_box, container, horizontal_space, row,
|
||||||
scrollable, text, text_editor, text_input,
|
scrollable, text, text_editor, text_input, tooltip,
|
||||||
},
|
},
|
||||||
Element, Font, Length, Task,
|
Element, Font, Length, Task,
|
||||||
};
|
};
|
||||||
use iced_video_player::Video;
|
use iced_video_player::Video;
|
||||||
|
use rfd::AsyncFileDialog;
|
||||||
use tracing::{debug, error};
|
use tracing::{debug, error};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
core::{service_items::ServiceTrait, songs::Song},
|
core::{service_items::ServiceTrait, songs::Song},
|
||||||
ui::slide_editor::{self, SlideEditor},
|
ui::{
|
||||||
|
slide_editor::{self, SlideEditor},
|
||||||
|
widgets::icon,
|
||||||
|
},
|
||||||
Background, BackgroundKind,
|
Background, BackgroundKind,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -129,7 +132,7 @@ impl SongEditor {
|
||||||
audio: PathBuf::new(),
|
audio: PathBuf::new(),
|
||||||
background: None,
|
background: None,
|
||||||
video: None,
|
video: None,
|
||||||
current_font: iced::font::default(),
|
current_font: iced::font::Font::DEFAULT,
|
||||||
ccli: "8".to_owned(),
|
ccli: "8".to_owned(),
|
||||||
slide_state: SlideEditor::default(),
|
slide_state: SlideEditor::default(),
|
||||||
}
|
}
|
||||||
|
@ -269,7 +272,7 @@ impl SongEditor {
|
||||||
let slide_preview = container(self.slide_preview())
|
let slide_preview = container(self.slide_preview())
|
||||||
.width(Length::FillPortion(2));
|
.width(Length::FillPortion(2));
|
||||||
|
|
||||||
let column = column::with_children(vec![
|
let column = column![
|
||||||
self.toolbar(),
|
self.toolbar(),
|
||||||
row![
|
row![
|
||||||
container(self.left_column())
|
container(self.left_column())
|
||||||
|
@ -278,8 +281,8 @@ impl SongEditor {
|
||||||
.center_x(Length::FillPortion(3))
|
.center_x(Length::FillPortion(3))
|
||||||
]
|
]
|
||||||
.into(),
|
.into(),
|
||||||
])
|
]
|
||||||
.spacing(theme::active().iced().space_l());
|
.spacing(15);
|
||||||
column.into()
|
column.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -332,37 +335,34 @@ impl SongEditor {
|
||||||
|
|
||||||
fn left_column(&self) -> Element<Message> {
|
fn left_column(&self) -> Element<Message> {
|
||||||
let title_input = text_input("song", &self.title)
|
let title_input = text_input("song", &self.title)
|
||||||
.on_input(Message::ChangeTitle)
|
.on_input(Message::ChangeTitle);
|
||||||
.label("Song Title");
|
|
||||||
|
|
||||||
let author_input = text_input("author", &self.author)
|
let author_input = text_input("author", &self.author)
|
||||||
.on_input(Message::ChangeAuthor)
|
.on_input(Message::ChangeAuthor);
|
||||||
.label("Song Author");
|
|
||||||
|
|
||||||
let verse_input = text_input(
|
let verse_input = text_input(
|
||||||
"Verse
|
"Verse
|
||||||
order",
|
order",
|
||||||
&self.verse_order,
|
&self.verse_order,
|
||||||
)
|
)
|
||||||
.label("Verse Order")
|
|
||||||
.on_input(Message::ChangeVerseOrder);
|
.on_input(Message::ChangeVerseOrder);
|
||||||
|
|
||||||
let lyric_title = text("Lyrics");
|
let lyric_title = text("Lyrics");
|
||||||
let lyric_input = column::with_children(vec![
|
let lyric_input = column![
|
||||||
lyric_title.into(),
|
lyric_title.into(),
|
||||||
text_editor(&self.lyrics)
|
text_editor(&self.lyrics)
|
||||||
.on_action(Message::ChangeLyrics)
|
.on_action(Message::ChangeLyrics)
|
||||||
.height(Length::Fill)
|
.height(Length::Fill)
|
||||||
.into(),
|
.into(),
|
||||||
])
|
]
|
||||||
.spacing(5);
|
.spacing(5);
|
||||||
|
|
||||||
column::with_children(vec![
|
column![
|
||||||
title_input.into(),
|
title_input.into(),
|
||||||
author_input.into(),
|
author_input.into(),
|
||||||
verse_input.into(),
|
verse_input.into(),
|
||||||
lyric_input.into(),
|
lyric_input.into(),
|
||||||
])
|
]
|
||||||
.spacing(25)
|
.spacing(25)
|
||||||
.width(Length::FillPortion(2))
|
.width(Length::FillPortion(2))
|
||||||
.into()
|
.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),
|
icon::from_name("folder-pictures-symbolic").scale(2),
|
||||||
)
|
"Background"
|
||||||
.label("Background")
|
))
|
||||||
.tooltip("Select an image or video background")
|
|
||||||
.on_press(Message::PickBackground)
|
.on_press(Message::PickBackground)
|
||||||
.padding(10);
|
.padding(10);
|
||||||
|
|
||||||
|
let background_selector = tooltip(
|
||||||
|
background_selector,
|
||||||
|
"Select an image or video background",
|
||||||
|
tooltip::Position::FollowCursor,
|
||||||
|
);
|
||||||
row![
|
row![
|
||||||
font_selector,
|
font_selector,
|
||||||
font_size,
|
font_size,
|
||||||
|
@ -448,18 +452,19 @@ impl Default for SongEditor {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn pick_background() -> Result<PathBuf, SongError> {
|
async fn pick_background() -> Result<PathBuf, SongError> {
|
||||||
let dialog = Dialog::new().title("Choose a background...");
|
// let dialog =
|
||||||
dialog
|
// AsyncFileDialog::new().set_title("Choose a background...");
|
||||||
.open_file()
|
// dialog
|
||||||
.await
|
|
||||||
.map_err(|_| SongError::DialogClosed)
|
|
||||||
.map(|file| file.url().to_file_path().unwrap())
|
|
||||||
// rfd::AsyncFileDialog::new()
|
|
||||||
// .set_title("Choose a background...")
|
|
||||||
// .pick_file()
|
// .pick_file()
|
||||||
// .await
|
// .await
|
||||||
// .ok_or(SongError::DialogClosed)
|
// .map_err(|_| SongError::DialogClosed)
|
||||||
// .map(|file| file.path().to_owned())
|
// .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)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
|
@ -7,7 +7,7 @@ use colors_transform::Rgb;
|
||||||
use iced::{
|
use iced::{
|
||||||
font::{Style, Weight},
|
font::{Style, Weight},
|
||||||
widget::{container, svg::Handle, Svg},
|
widget::{container, svg::Handle, Svg},
|
||||||
Length, Size,
|
Element, Length, Size,
|
||||||
};
|
};
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
|
@ -47,9 +47,7 @@ impl From<iced::font::Font> for Font {
|
||||||
fn from(value: iced::font::Font) -> Self {
|
fn from(value: iced::font::Font) -> Self {
|
||||||
Self {
|
Self {
|
||||||
name: match value.family {
|
name: match value.family {
|
||||||
iced::iced::font::Family::Name(name) => {
|
iced::font::Family::Name(name) => name.to_string(),
|
||||||
name.to_string()
|
|
||||||
}
|
|
||||||
_ => "Quicksand Bold".into(),
|
_ => "Quicksand Bold".into(),
|
||||||
},
|
},
|
||||||
size: 20,
|
size: 20,
|
||||||
|
|
115
src/ui/widgets/icon/handle.rs
Normal file
115
src/ui/widgets/icon/handle.rs
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
// Copyright 2023 System76 <info@system76.com>
|
||||||
|
// 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<Cow<'static, [u8]>>
|
||||||
|
+ 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<Cow<'static, [u8]>>
|
||||||
|
+ 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<Cow<'static, [u8]>>,
|
||||||
|
) -> Handle {
|
||||||
|
Handle {
|
||||||
|
symbolic: false,
|
||||||
|
data: Data::Svg(svg::Handle::from_memory(bytes)),
|
||||||
|
}
|
||||||
|
}
|
187
src/ui/widgets/icon/mod.rs
Normal file
187
src/ui/widgets/icon/mod.rs
Normal file
|
@ -0,0 +1,187 @@
|
||||||
|
// Copyright 2022 System76 <info@system76.com>
|
||||||
|
// 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<Arc<str>>) -> 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<Length>,
|
||||||
|
#[setters(strip_option)]
|
||||||
|
height: Option<Length>,
|
||||||
|
#[setters(strip_option)]
|
||||||
|
rotation: Option<Rotation>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Icon {
|
||||||
|
#[must_use]
|
||||||
|
pub fn into_svg_handle(
|
||||||
|
self,
|
||||||
|
) -> Option<iced::widget::svg::Handle> {
|
||||||
|
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::<crate::Theme>::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<Icon> for Element<'a, Message> {
|
||||||
|
fn from(icon: Icon) -> Self {
|
||||||
|
icon.view::<Message>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 => {}
|
||||||
|
}
|
||||||
|
}
|
165
src/ui/widgets/icon/named.rs
Normal file
165
src/ui/widgets/icon/named.rs
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
// Copyright 2023 System76 <info@system76.com>
|
||||||
|
// 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<Cow<'static, str>>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<str>,
|
||||||
|
|
||||||
|
/// Checks for a fallback if the icon was not found.
|
||||||
|
pub fallback: Option<IconFallback>,
|
||||||
|
|
||||||
|
/// Restrict the lookup to a given scale.
|
||||||
|
#[setters(strip_option)]
|
||||||
|
pub scale: Option<u16>,
|
||||||
|
|
||||||
|
/// Restrict the lookup to a given size.
|
||||||
|
#[setters(strip_option)]
|
||||||
|
pub size: Option<u16>,
|
||||||
|
|
||||||
|
/// 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<Arc<str>>) -> 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<PathBuf> {
|
||||||
|
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<PathBuf> {
|
||||||
|
//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<Named> for Handle {
|
||||||
|
#[inline]
|
||||||
|
fn from(builder: Named) -> Self {
|
||||||
|
builder.handle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Named> for Icon {
|
||||||
|
#[inline]
|
||||||
|
fn from(builder: Named) -> Self {
|
||||||
|
builder.icon()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message: 'static> From<Named> for crate::Element<'_, Message> {
|
||||||
|
#[inline]
|
||||||
|
fn from(builder: Named) -> Self {
|
||||||
|
builder.icon().into()
|
||||||
|
}
|
||||||
|
}
|
|
@ -1 +1,2 @@
|
||||||
// pub mod slide_text;
|
// pub mod slide_text;
|
||||||
|
pub mod icon;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue