well crap
Some checks are pending
/ test (push) Waiting to run

This commit is contained in:
Chris Cochrun 2025-08-26 15:25:04 -05:00
parent 4ae6a9a9a7
commit 1861f357a8
16 changed files with 1026 additions and 562 deletions

98
Cargo.lock generated
View file

@ -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"

View file

@ -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"

View file

@ -1,6 +1,5 @@
use std::mem::replace;
use iced::iced::Executor;
use miette::{miette, Result};
use sqlx::{Connection, SqliteConnection};

View file

@ -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<u8>, 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<std::borrow::Cow<'static, [u8]>> {
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<std::borrow::Cow<'static, [u8]>> {
// 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 {

View file

@ -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::{

View file

@ -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::{

View file

@ -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<ServiceItem>),
}
#[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<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");
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<Element<Message>> {
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::<ServiceItem>( 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::<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,
))
}
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<Element<Self::Message>> {
fn header_start(&self) -> Vec<Element<Message>> {
vec![]
}
fn header_center(&self) -> Vec<Element<Self::Message>> {
vec![search_input("Search...", "")
fn header_center(&self) -> Vec<Element<Message>> {
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<Element<Self::Message>> {
fn header_end(&self) -> Vec<Element<Message>> {
// 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<Element<Self::Message>> {
fn footer(&self) -> Option<Element<Message>> {
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<Self::Message> {
fn subscription(&self) -> Subscription<Message> {
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<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>> {
fn dialog(&self) -> Option<Element<'_, Message>> {
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<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_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<Message> {
fn view_presenter(&self) -> Element<Message> {
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<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 update_title(&mut self) -> Task<Message> {
// let window_title = "Lumina";
// self.windows.first_key_value().unwrap().1.title =
// window_title.to_string();
// Task::none()
// }
fn show_window(&mut self) -> Task<Message> {
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<Message> {
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 {

View file

@ -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::<Message>().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::<Message, ServiceItem>::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()
}

View file

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

View file

@ -49,7 +49,7 @@ pub enum SlideError {
#[derive(Debug, Default)]
struct EditorProgram {
mouse_button_pressed: Option<iced::iced::mouse::Button>,
mouse_button_pressed: Option<iced::mouse::Button>,
}
impl SlideEditor {
@ -78,8 +78,8 @@ impl<'a> Program<SlideWidget, iced::Theme, iced::Renderer>
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<canvas::Geometry<Renderer>> {
// We prepare a new `Frame`
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
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<SlideWidget, iced::Theme, iced::Renderer>
fn update(
&self,
_state: &mut Self::State,
event: canvas::Event,
bounds: iced::iced::Rectangle,
_cursor: iced::iced_core::mouse::Cursor,
) -> (canvas::event::Status, Option<SlideWidget>) {
event: &iced::Event,
bounds: iced::Rectangle,
_cursor: iced::mouse::Cursor,
) -> std::option::Option<iced::widget::Action<SlideWidget>> {
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<SlideWidget, iced::Theme, iced::Renderer>
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()
}
}

View file

@ -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<Message> {
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<PathBuf, SongError> {
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)]

View file

@ -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<iced::font::Font> 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,

View 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
View 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 => {}
}
}

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

View file

@ -1 +1,2 @@
// pub mod slide_text;
pub mod icon;