Compare commits
11 commits
iced-switc
...
master
Author | SHA1 | Date | |
---|---|---|---|
fae83aedc5 | |||
035f8896f1 | |||
6c8cb6c5b2 | |||
d6b4cc6297 | |||
4792304d8b | |||
23cd34388b | |||
4ccb186189 | |||
1446e35c58 | |||
5f3d867ad7 | |||
aa5e7420f1 | |||
213e47bf6d |
22 changed files with 2939 additions and 2138 deletions
2352
Cargo.lock
generated
2352
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
22
Cargo.toml
22
Cargo.toml
|
@ -8,7 +8,6 @@ description = "A cli presentation system"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = { version = "4.5.20", features = ["debug", "derive"] }
|
clap = { version = "4.5.20", features = ["debug", "derive"] }
|
||||||
# libcosmic = { git = "https://github.com/pop-os/libcosmic", default-features = false, features = ["debug", "winit", "desktop", "winit_wgpu", "winit_tokio", "tokio", "rfd", "dbus-config", "a11y", "wgpu", "multi-window"] }
|
|
||||||
lexpr = "0.2.7"
|
lexpr = "0.2.7"
|
||||||
miette = { version = "7.2.0", features = ["fancy"] }
|
miette = { version = "7.2.0", features = ["fancy"] }
|
||||||
pretty_assertions = "1.4.1"
|
pretty_assertions = "1.4.1"
|
||||||
|
@ -33,23 +32,22 @@ gstreamer-app = "0.23"
|
||||||
url = "2"
|
url = "2"
|
||||||
colors-transform = "0.2.11"
|
colors-transform = "0.2.11"
|
||||||
rayon = "1.11.0"
|
rayon = "1.11.0"
|
||||||
# resvg = "0.45.1"
|
resvg = "0.45.1"
|
||||||
|
image = "0.25.8"
|
||||||
# femtovg = { version = "0.16.0", features = ["wgpu"] }
|
# femtovg = { version = "0.16.0", features = ["wgpu"] }
|
||||||
# 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.libcosmic]
|
||||||
git = "https://github.com/iced-rs/iced"
|
git = "https://github.com/pop-os/libcosmic"
|
||||||
branch = "master"
|
default-features = false
|
||||||
features = ["wgpu", "image", "advanced", "svg", "canvas", "hot", "debug", "lazy", "tokio"]
|
features = ["debug", "winit", "desktop", "winit_wgpu", "winit_tokio", "tokio", "rfd", "dbus-config", "a11y", "wgpu", "multi-window"]
|
||||||
|
|
||||||
[dependencies.iced_video_player]
|
[dependencies.iced_video_player]
|
||||||
git = "https://git.tfcconnection.org/chris/iced_video_player"
|
git = "https://github.com/jackpot51/iced_video_player.git"
|
||||||
branch = "master"
|
branch = "cosmic"
|
||||||
# branch = "cosmic"
|
features = ["wgpu"]
|
||||||
|
|
||||||
# [profile.dev]
|
# [profile.dev]
|
||||||
# opt-level = 3
|
# opt-level = 3
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use std::mem::replace;
|
use std::mem::replace;
|
||||||
|
|
||||||
|
use cosmic::iced::Executor;
|
||||||
use miette::{miette, Result};
|
use miette::{miette, Result};
|
||||||
use sqlx::{Connection, SqliteConnection};
|
use sqlx::{Connection, SqliteConnection};
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::sync::Arc;
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
use cosmic::iced::clipboard::mime::{AllowedMimeTypes, AsMimeTypes};
|
||||||
use crisp::types::{Keyword, Symbol, Value};
|
use crisp::types::{Keyword, Symbol, Value};
|
||||||
// use cosmic::iced::clipboard::mime::{AllowedMimeTypes, AsMimeTypes};
|
|
||||||
use miette::Result;
|
use miette::Result;
|
||||||
|
use resvg::usvg::fontdb;
|
||||||
use tracing::{debug, error};
|
use tracing::{debug, error};
|
||||||
|
|
||||||
use crate::Slide;
|
use crate::Slide;
|
||||||
|
@ -17,13 +18,13 @@ use super::videos::Video;
|
||||||
|
|
||||||
use super::kinds::ServiceItemKind;
|
use super::kinds::ServiceItemKind;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub struct ServiceItem {
|
pub struct ServiceItem {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub database_id: i32,
|
pub database_id: i32,
|
||||||
pub kind: ServiceItemKind,
|
pub kind: ServiceItemKind,
|
||||||
pub slides: Arc<[Slide]>,
|
pub slides: Vec<Slide>,
|
||||||
// pub item: Box<dyn ServiceTrait>,
|
// pub item: Box<dyn ServiceTrait>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,29 +57,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 {
|
||||||
|
@ -121,7 +122,7 @@ impl Default for ServiceItem {
|
||||||
title: String::default(),
|
title: String::default(),
|
||||||
database_id: 0,
|
database_id: 0,
|
||||||
kind: ServiceItemKind::Content(Slide::default()),
|
kind: ServiceItemKind::Content(Slide::default()),
|
||||||
slides: Arc::new([]),
|
slides: vec![],
|
||||||
// item: Box::new(Image::default()),
|
// item: Box::new(Image::default()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -171,7 +172,7 @@ impl From<&Value> for ServiceItem {
|
||||||
kind: ServiceItemKind::Content(
|
kind: ServiceItemKind::Content(
|
||||||
slide.clone(),
|
slide.clone(),
|
||||||
),
|
),
|
||||||
slides: Arc::new([slide]),
|
slides: vec![slide],
|
||||||
}
|
}
|
||||||
} else if let Some(background) =
|
} else if let Some(background) =
|
||||||
list.get(background_pos)
|
list.get(background_pos)
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
// use iced::dialog::ashpd::url::Url;
|
// use cosmic::dialog::ashpd::url::Url;
|
||||||
use crisp::types::{Keyword, Symbol, Value};
|
use crisp::types::{Keyword, Symbol, Value};
|
||||||
use iced_video_player::Video;
|
use iced_video_player::Video;
|
||||||
use miette::{miette, Result};
|
use miette::{miette, Result};
|
||||||
|
use resvg::usvg::fontdb;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
fmt::Display,
|
fmt::Display,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
|
sync::Arc,
|
||||||
};
|
};
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
|
@ -13,6 +15,40 @@ use crate::ui::text_svg::{self, TextSvg};
|
||||||
|
|
||||||
use super::songs::Song;
|
use super::songs::Song;
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
Clone, Debug, Default, PartialEq, Serialize, Deserialize,
|
||||||
|
)]
|
||||||
|
pub struct Slide {
|
||||||
|
id: i32,
|
||||||
|
background: Background,
|
||||||
|
text: String,
|
||||||
|
font: String,
|
||||||
|
font_size: i32,
|
||||||
|
text_alignment: TextAlignment,
|
||||||
|
audio: Option<PathBuf>,
|
||||||
|
video_loop: bool,
|
||||||
|
video_start_time: f32,
|
||||||
|
video_end_time: f32,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub text_svg: Option<TextSvg>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize,
|
||||||
|
)]
|
||||||
|
pub enum BackgroundKind {
|
||||||
|
#[default]
|
||||||
|
Image,
|
||||||
|
Video,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
struct Image {
|
||||||
|
pub source: String,
|
||||||
|
pub fit: String,
|
||||||
|
pub children: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(
|
#[derive(
|
||||||
Clone,
|
Clone,
|
||||||
Copy,
|
Copy,
|
||||||
|
@ -203,15 +239,6 @@ impl Display for ParseError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(
|
|
||||||
Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize,
|
|
||||||
)]
|
|
||||||
pub enum BackgroundKind {
|
|
||||||
#[default]
|
|
||||||
Image,
|
|
||||||
Video,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<String> for BackgroundKind {
|
impl From<String> for BackgroundKind {
|
||||||
fn from(value: String) -> Self {
|
fn from(value: String) -> Self {
|
||||||
if value == "image" {
|
if value == "image" {
|
||||||
|
@ -222,24 +249,6 @@ impl From<String> for BackgroundKind {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(
|
|
||||||
Clone, Debug, Default, PartialEq, Serialize, Deserialize,
|
|
||||||
)]
|
|
||||||
pub struct Slide {
|
|
||||||
id: i32,
|
|
||||||
background: Background,
|
|
||||||
text: String,
|
|
||||||
font: String,
|
|
||||||
font_size: i32,
|
|
||||||
text_alignment: TextAlignment,
|
|
||||||
audio: Option<PathBuf>,
|
|
||||||
video_loop: bool,
|
|
||||||
video_start_time: f32,
|
|
||||||
video_end_time: f32,
|
|
||||||
#[serde(skip)]
|
|
||||||
pub text_svg: TextSvg,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&Slide> for Value {
|
impl From<&Slide> for Value {
|
||||||
fn from(value: &Slide) -> Self {
|
fn from(value: &Slide) -> Self {
|
||||||
Self::List(vec![Self::Symbol(Symbol("slide".into()))])
|
Self::List(vec![Self::Symbol(Symbol("slide".into()))])
|
||||||
|
@ -252,6 +261,11 @@ impl Slide {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_text_svg(mut self, text_svg: TextSvg) -> Self {
|
||||||
|
self.text_svg = Some(text_svg);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_font(mut self, font: impl AsRef<str>) -> Self {
|
pub fn set_font(mut self, font: impl AsRef<str>) -> Self {
|
||||||
self.font = font.as_ref().into();
|
self.font = font.as_ref().into();
|
||||||
self
|
self
|
||||||
|
@ -275,6 +289,10 @@ impl Slide {
|
||||||
self.text.clone()
|
self.text.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn text_alignment(&self) -> TextAlignment {
|
||||||
|
self.text_alignment.clone()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn font_size(&self) -> i32 {
|
pub fn font_size(&self) -> i32 {
|
||||||
self.font_size
|
self.font_size
|
||||||
}
|
}
|
||||||
|
@ -614,7 +632,6 @@ impl SlideBuilder {
|
||||||
let Some(video_end_time) = self.video_end_time else {
|
let Some(video_end_time) = self.video_end_time else {
|
||||||
return Err(miette!("No video_end_time"));
|
return Err(miette!("No video_end_time"));
|
||||||
};
|
};
|
||||||
if let Some(text_svg) = self.text_svg {
|
|
||||||
Ok(Slide {
|
Ok(Slide {
|
||||||
background,
|
background,
|
||||||
text,
|
text,
|
||||||
|
@ -625,42 +642,10 @@ impl SlideBuilder {
|
||||||
video_loop,
|
video_loop,
|
||||||
video_start_time,
|
video_start_time,
|
||||||
video_end_time,
|
video_end_time,
|
||||||
text_svg,
|
text_svg: self.text_svg,
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
let text_svg = TextSvg::new(text.clone())
|
|
||||||
.alignment(text_alignment)
|
|
||||||
.fill("#fff")
|
|
||||||
.shadow(text_svg::shadow(2, 2, 5, "#000000"))
|
|
||||||
.stroke(text_svg::stroke(3, "#000"))
|
|
||||||
.font(
|
|
||||||
text_svg::Font::from(font.clone())
|
|
||||||
.size(font_size.try_into().unwrap()),
|
|
||||||
)
|
|
||||||
.build();
|
|
||||||
Ok(Slide {
|
|
||||||
background,
|
|
||||||
text,
|
|
||||||
font,
|
|
||||||
font_size,
|
|
||||||
text_alignment,
|
|
||||||
audio: self.audio,
|
|
||||||
video_loop,
|
|
||||||
video_start_time,
|
|
||||||
video_end_time,
|
|
||||||
text_svg,
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default)]
|
|
||||||
struct Image {
|
|
||||||
pub source: String,
|
|
||||||
pub fit: String,
|
|
||||||
pub children: Vec<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Image {
|
impl Image {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use std::{collections::HashMap, option::Option, path::PathBuf};
|
use std::{collections::HashMap, option::Option, path::PathBuf};
|
||||||
|
|
||||||
|
use cosmic::iced::Executor;
|
||||||
use crisp::types::{Keyword, Symbol, Value};
|
use crisp::types::{Keyword, Symbol, Value};
|
||||||
use miette::{miette, IntoDiagnostic, Result};
|
use miette::{miette, IntoDiagnostic, Result};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
|
@ -7,6 +7,7 @@ use super::{
|
||||||
service_items::ServiceTrait,
|
service_items::ServiceTrait,
|
||||||
slide::Slide,
|
slide::Slide,
|
||||||
};
|
};
|
||||||
|
use cosmic::iced::Executor;
|
||||||
use crisp::types::{Keyword, Symbol, Value};
|
use crisp::types::{Keyword, Symbol, Value};
|
||||||
use miette::{IntoDiagnostic, Result};
|
use miette::{IntoDiagnostic, Result};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
916
src/main.rs
916
src/main.rs
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,6 @@
|
||||||
use std::ops::RangeInclusive;
|
use std::ops::RangeInclusive;
|
||||||
|
|
||||||
use iced::Length;
|
use cosmic::iced::Length;
|
||||||
|
|
||||||
struct DoubleSlider<'a, T, Message> {
|
struct DoubleSlider<'a, T, Message> {
|
||||||
range: RangeInclusive<T>,
|
range: RangeInclusive<T>,
|
||||||
|
|
|
@ -1,21 +1,23 @@
|
||||||
use iced::{
|
use cosmic::{
|
||||||
advanced::widget::{tree::State, Widget},
|
iced::{
|
||||||
alignment::Vertical,
|
alignment::Vertical, clipboard::dnd::DndAction,
|
||||||
futures::FutureExt,
|
futures::FutureExt, Background, Border, Color, Length,
|
||||||
|
},
|
||||||
|
iced_core::widget::tree::State,
|
||||||
|
iced_widget::{column, row as rowm, text as textm},
|
||||||
theme,
|
theme,
|
||||||
widget::{
|
widget::{
|
||||||
button, column, container, horizontal_space, mouse_area,
|
button, container, horizontal_space, icon, mouse_area,
|
||||||
responsive, row, scrollable, text, text_input, Container,
|
responsive, row, scrollable, text, text_input, Container,
|
||||||
Space,
|
DndSource, Space, Widget,
|
||||||
},
|
},
|
||||||
Background, Border, Color, Element, Length, Task,
|
Element, 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::{
|
use crate::core::{
|
||||||
core::{
|
|
||||||
content::Content,
|
content::Content,
|
||||||
images::{update_image_in_db, Image},
|
images::{update_image_in_db, Image},
|
||||||
model::{LibraryKind, Model},
|
model::{LibraryKind, Model},
|
||||||
|
@ -23,8 +25,6 @@ use crate::{
|
||||||
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)]
|
||||||
|
@ -264,12 +264,18 @@ impl<'a> Library {
|
||||||
let presentation_library =
|
let presentation_library =
|
||||||
self.library_item(&self.presentation_library);
|
self.library_item(&self.presentation_library);
|
||||||
let column = column![
|
let column = column![
|
||||||
|
text::heading("Library").center().width(Length::Fill),
|
||||||
|
cosmic::iced::widget::horizontal_rule(1),
|
||||||
song_library,
|
song_library,
|
||||||
image_library,
|
image_library,
|
||||||
video_library,
|
video_library,
|
||||||
presentation_library,
|
presentation_library,
|
||||||
];
|
]
|
||||||
column.height(Length::Fill).spacing(5).into()
|
.height(Length::Fill)
|
||||||
|
.padding(10)
|
||||||
|
.spacing(10)
|
||||||
|
.into();
|
||||||
|
column
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn library_item<T>(
|
pub fn library_item<T>(
|
||||||
|
@ -279,40 +285,40 @@ impl<'a> Library {
|
||||||
where
|
where
|
||||||
T: Content,
|
T: Content,
|
||||||
{
|
{
|
||||||
let mut row = row![].spacing(5);
|
let mut row = row::<Message>().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
|
||||||
row.push(text("Songs").align_y(Vertical::Center));
|
.push(textm!("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(text("Videos").align_y(Vertical::Center));
|
.push(textm!("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(text("Images").align_y(Vertical::Center));
|
.push(textm!("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(
|
||||||
text("Presentations").align_y(Vertical::Center),
|
textm!("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(text!("{}", item_count).align_y(Vertical::Center));
|
.push(textm!("{}", 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) {
|
||||||
|
@ -332,26 +338,19 @@ 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.extended_palette()
|
t.cosmic().button.hover.into()
|
||||||
.primary
|
|
||||||
.strong
|
|
||||||
.color
|
|
||||||
} else {
|
} else {
|
||||||
t.extended_palette()
|
t.cosmic().button.base.into()
|
||||||
.background
|
|
||||||
.base
|
|
||||||
.color
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
None => Background::Color(
|
None => Background::Color(
|
||||||
t.extended_palette()
|
t.cosmic().button.base.into(),
|
||||||
.background
|
|
||||||
.base
|
|
||||||
.color,
|
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.border(Border::default().rounded(5))
|
.border(Border::default().rounded(
|
||||||
|
t.cosmic().corner_radii.radius_s,
|
||||||
|
))
|
||||||
})
|
})
|
||||||
.center_x(Length::Fill)
|
.center_x(Length::Fill)
|
||||||
.center_y(Length::Shrink);
|
.center_y(Length::Shrink);
|
||||||
|
@ -374,21 +373,52 @@ 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(
|
.on_drag(Message::DragItem(service_item.clone()))
|
||||||
// service_item.clone(),
|
.on_enter(Message::HoverItem(
|
||||||
// ))
|
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(Some(
|
.on_press(Message::SelectItem(
|
||||||
(model.kind, index as i32),
|
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()
|
.into()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -399,9 +429,9 @@ impl<'a> Library {
|
||||||
.spacing(5)
|
.spacing(5)
|
||||||
.height(Length::Fill);
|
.height(Length::Fill);
|
||||||
|
|
||||||
let library_toolbar = row!(
|
let library_toolbar = rowm!(
|
||||||
text_input("Search...", ""),
|
text_input("Search...", ""),
|
||||||
button(icon::from_name("add"))
|
button::icon(icon::from_name("add"))
|
||||||
);
|
);
|
||||||
let library_column =
|
let library_column =
|
||||||
column![library_toolbar, items].spacing(3);
|
column![library_toolbar, items].spacing(3);
|
||||||
|
@ -422,36 +452,67 @@ impl<'a> Library {
|
||||||
where
|
where
|
||||||
T: Content,
|
T: Content,
|
||||||
{
|
{
|
||||||
let item_text = Container::new(responsive(|size| {
|
let text = Container::new(responsive(|size| {
|
||||||
text(elide_text(item.title(), size.width))
|
text::heading(elide_text(item.title(), size.width))
|
||||||
.center()
|
.center()
|
||||||
.wrapping(text::Wrapping::None)
|
.wrapping(textm::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(move |size| {
|
||||||
if item.background().is_some() {
|
let color: Color = if item.background().is_some() {
|
||||||
text(elide_text(item.subtext(), size.width))
|
if let Some((library, selected)) = self.selected_item
|
||||||
.style(text::primary)
|
{
|
||||||
.center()
|
if model.kind == library
|
||||||
.wrapping(text::Wrapping::None)
|
&& selected == index as i32
|
||||||
.into()
|
{
|
||||||
|
theme::active().cosmic().control_0().into()
|
||||||
} else {
|
} else {
|
||||||
text(elide_text(item.subtext(), size.width))
|
theme::active()
|
||||||
.style(text::primary)
|
.cosmic()
|
||||||
.center()
|
.accent_text_color()
|
||||||
.wrapping(text::Wrapping::None)
|
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
theme::active()
|
||||||
|
.cosmic()
|
||||||
|
.accent_text_color()
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let Some((library, selected)) = self.selected_item
|
||||||
|
{
|
||||||
|
if model.kind == library
|
||||||
|
&& selected == index as i32
|
||||||
|
{
|
||||||
|
theme::active().cosmic().control_0().into()
|
||||||
|
} else {
|
||||||
|
theme::active()
|
||||||
|
.cosmic()
|
||||||
|
.destructive_text_color()
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
theme::active()
|
||||||
|
.cosmic()
|
||||||
|
.destructive_text_color()
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
text::body(elide_text(item.subtext(), size.width))
|
||||||
|
.center()
|
||||||
|
.wrapping(textm::Wrapping::None)
|
||||||
|
.class(color)
|
||||||
|
.into()
|
||||||
}))
|
}))
|
||||||
.center_y(20)
|
.center_y(20)
|
||||||
.center_x(Length::Fill);
|
.center_x(Length::Fill);
|
||||||
|
|
||||||
let texts = column([item_text.into(), subtext.into()]);
|
let texts = column([text.into(), subtext.into()]);
|
||||||
|
|
||||||
Container::new(
|
Container::new(
|
||||||
row![horizontal_space().width(0), texts]
|
rowm![horizontal_space().width(0), texts]
|
||||||
.spacing(10)
|
.spacing(10)
|
||||||
.align_y(Vertical::Center),
|
.align_y(Vertical::Center),
|
||||||
)
|
)
|
||||||
|
@ -466,9 +527,21 @@ impl<'a> Library {
|
||||||
if model.kind == library
|
if model.kind == library
|
||||||
&& selected == index as i32
|
&& selected == index as i32
|
||||||
{
|
{
|
||||||
t.extended_palette().primary.strong.color
|
t.cosmic().accent.selected.into()
|
||||||
} else {
|
} else {
|
||||||
t.extended_palette().primary.base.color
|
if let Some((library, hovered)) =
|
||||||
|
self.hovered_item
|
||||||
|
{
|
||||||
|
if model.kind == library
|
||||||
|
&& hovered == index as i32
|
||||||
|
{
|
||||||
|
t.cosmic().button.hover.into()
|
||||||
|
} else {
|
||||||
|
t.cosmic().button.base.into()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
t.cosmic().button.base.into()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if let Some((library, hovered)) =
|
} else if let Some((library, hovered)) =
|
||||||
self.hovered_item
|
self.hovered_item
|
||||||
|
@ -476,16 +549,20 @@ impl<'a> Library {
|
||||||
if model.kind == library
|
if model.kind == library
|
||||||
&& hovered == index as i32
|
&& hovered == index as i32
|
||||||
{
|
{
|
||||||
t.extended_palette().primary.strong.color
|
t.cosmic().button.hover.into()
|
||||||
} else {
|
} else {
|
||||||
t.extended_palette().primary.base.color
|
t.cosmic().button.base.into()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
t.extended_palette().background.strong.color
|
t.cosmic().button.base.into()
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
.border(Border::default().rounded(10))
|
.border(
|
||||||
|
Border::default()
|
||||||
|
.rounded(t.cosmic().corner_radii.radius_m),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
.padding([3, 0])
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,29 +1,36 @@
|
||||||
use miette::{IntoDiagnostic, Result};
|
use miette::{IntoDiagnostic, Result};
|
||||||
use std::{fs::File, io::BufReader, path::PathBuf, sync::Arc};
|
use std::{fs::File, io::BufReader, path::PathBuf, sync::Arc};
|
||||||
|
|
||||||
use iced::{
|
use cosmic::{
|
||||||
|
iced::{
|
||||||
alignment::Horizontal,
|
alignment::Horizontal,
|
||||||
border,
|
border,
|
||||||
font::{Family, Stretch, Style, Weight},
|
font::{Family, Stretch, Style, Weight},
|
||||||
widget::{
|
Background, Border, Color, ContentFit, Font, Length, Shadow,
|
||||||
container, image, mouse_area, responsive, rich_text,
|
Vector,
|
||||||
|
},
|
||||||
|
iced_widget::{
|
||||||
|
rich_text,
|
||||||
scrollable::{
|
scrollable::{
|
||||||
self, scroll_to, AbsoluteOffset, Direction, Id, Scrollbar,
|
scroll_to, AbsoluteOffset, Direction, Scrollbar,
|
||||||
},
|
},
|
||||||
span, stack, text, vertical_rule, Column, Container, Row,
|
span, stack, vertical_rule,
|
||||||
Space,
|
|
||||||
},
|
},
|
||||||
Background, Border, Color, ContentFit, Element, Font, Length,
|
prelude::*,
|
||||||
Shadow, Task, Vector,
|
widget::{
|
||||||
|
container, image, mouse_area, responsive, scrollable, text,
|
||||||
|
Column, Container, Id, Image, Row, Space,
|
||||||
|
},
|
||||||
|
Task,
|
||||||
};
|
};
|
||||||
use iced_video_player::{Position, Video, VideoPlayer};
|
use iced_video_player::{gst_pbutils, Position, Video, VideoPlayer};
|
||||||
use rodio::{Decoder, OutputStream, Sink};
|
use rodio::{Decoder, OutputStream, Sink};
|
||||||
use tracing::{debug, error, info, warn};
|
use tracing::{debug, error, info, warn};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
core::{service_items::ServiceItem, slide::Slide},
|
core::{service_items::ServiceItem, slide::Slide},
|
||||||
// ui::widgets::slide_text,
|
ui::text_svg,
|
||||||
BackgroundKind,
|
BackgroundKind,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -125,7 +132,9 @@ impl Presenter {
|
||||||
Some(v)
|
Some(v)
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Had an error creating the video object: {e}, likely the first slide isn't a video");
|
error!(
|
||||||
|
"Had an error creating the video object: {e}, likely the first slide isn't a video"
|
||||||
|
);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -141,6 +150,7 @@ impl Presenter {
|
||||||
};
|
};
|
||||||
let total_slides: usize =
|
let total_slides: usize =
|
||||||
items.iter().fold(0, |a, item| a + item.slides.len());
|
items.iter().fold(0, |a, item| a + item.slides.len());
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
current_slide: items[0].slides[0].clone(),
|
current_slide: items[0].slides[0].clone(),
|
||||||
current_item: 0,
|
current_item: 0,
|
||||||
|
@ -161,7 +171,7 @@ impl Presenter {
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
scroll_id: Id::unique(),
|
scroll_id: Id::unique(),
|
||||||
current_font: iced::font::Font::DEFAULT,
|
current_font: cosmic::font::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -337,27 +347,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
|
||||||
|
@ -370,7 +380,7 @@ impl Presenter {
|
||||||
self.hovered_slide = slide;
|
self.hovered_slide = slide;
|
||||||
}
|
}
|
||||||
Message::StartAudio => {
|
Message::StartAudio => {
|
||||||
return Action::Task(self.start_audio())
|
return Action::Task(self.start_audio());
|
||||||
}
|
}
|
||||||
Message::EndAudio => {
|
Message::EndAudio => {
|
||||||
self.sink.1.stop();
|
self.sink.1.stop();
|
||||||
|
@ -442,7 +452,7 @@ impl Presenter {
|
||||||
.style(move |t| {
|
.style(move |t| {
|
||||||
let mut style =
|
let mut style =
|
||||||
container::Style::default();
|
container::Style::default();
|
||||||
let theme = t;
|
let theme = t.cosmic();
|
||||||
let hovered = self.hovered_slide
|
let hovered = self.hovered_slide
|
||||||
== Some((
|
== Some((
|
||||||
item_index,
|
item_index,
|
||||||
|
@ -452,25 +462,19 @@ impl Presenter {
|
||||||
Some(Background::Color(
|
Some(Background::Color(
|
||||||
if is_current_slide {
|
if is_current_slide {
|
||||||
theme
|
theme
|
||||||
.extended_palette(
|
.accent
|
||||||
)
|
.base
|
||||||
.secondary
|
.into()
|
||||||
.strong
|
|
||||||
.color
|
|
||||||
} else if hovered {
|
} else if hovered {
|
||||||
theme
|
theme
|
||||||
.extended_palette(
|
.accent
|
||||||
)
|
.hover
|
||||||
.secondary
|
.into()
|
||||||
.strong
|
|
||||||
.color
|
|
||||||
} else {
|
} else {
|
||||||
theme
|
theme
|
||||||
.extended_palette(
|
.palette
|
||||||
)
|
.neutral_3
|
||||||
.background
|
.into()
|
||||||
.neutral
|
|
||||||
.color
|
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
style.border = Border::default()
|
style.border = Border::default()
|
||||||
|
@ -503,7 +507,7 @@ impl Presenter {
|
||||||
.padding(10),
|
.padding(10),
|
||||||
)
|
)
|
||||||
.interaction(
|
.interaction(
|
||||||
iced::mouse::Interaction::Pointer,
|
cosmic::iced::mouse::Interaction::Pointer,
|
||||||
)
|
)
|
||||||
.on_move(move |_| {
|
.on_move(move |_| {
|
||||||
Message::HoveredSlide(Some((
|
Message::HoveredSlide(Some((
|
||||||
|
@ -521,11 +525,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(item.title.clone());
|
let label = text::body(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, 35]);
|
.padding([0, 0, 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))
|
||||||
|
@ -535,12 +539,11 @@ impl Presenter {
|
||||||
items.push(divider.into());
|
items.push(divider.into());
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
let row = scrollable::Scrollable::new(
|
let row =
|
||||||
container(Row::from_vec(items)).style(|t| {
|
scrollable(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)
|
||||||
|
@ -578,7 +581,7 @@ impl Presenter {
|
||||||
// Container::new(container)
|
// Container::new(container)
|
||||||
// .style(move |t| {
|
// .style(move |t| {
|
||||||
// let mut style = container::Style::default();
|
// let mut style = container::Style::default();
|
||||||
// let theme = t.iced();
|
// let theme = t.cosmic();
|
||||||
// let hovered = self.hovered_slide == slide_id;
|
// let hovered = self.hovered_slide == slide_id;
|
||||||
// style.background = Some(Background::Color(
|
// style.background = Some(Background::Color(
|
||||||
// if is_current_slide {
|
// if is_current_slide {
|
||||||
|
@ -617,7 +620,7 @@ impl Presenter {
|
||||||
// .height(100)
|
// .height(100)
|
||||||
// .padding(10),
|
// .padding(10),
|
||||||
// )
|
// )
|
||||||
// .interaction(iced::iced::mouse::Interaction::Pointer)
|
// .interaction(cosmic::iced::mouse::Interaction::Pointer)
|
||||||
// .on_move(move |_| Message::HoveredSlide(slide_id))
|
// .on_move(move |_| Message::HoveredSlide(slide_id))
|
||||||
// .on_exit(Message::HoveredSlide(-1))
|
// .on_exit(Message::HoveredSlide(-1))
|
||||||
// .on_press(Message::SlideChange(slide.clone()));
|
// .on_press(Message::SlideChange(slide.clone()));
|
||||||
|
@ -640,7 +643,9 @@ impl Presenter {
|
||||||
self.video = Some(v)
|
self.video = Some(v)
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Had an error creating the video object: {e}");
|
error!(
|
||||||
|
"Had an error creating the video object: {e}"
|
||||||
|
);
|
||||||
self.video = None;
|
self.video = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -702,56 +707,8 @@ pub(crate) fn slide_view(
|
||||||
let slide_text = slide.text();
|
let slide_text = slide.text();
|
||||||
|
|
||||||
// let font = SvgFont::from(font).size(font_size.floor() as u8);
|
// let font = SvgFont::from(font).size(font_size.floor() as u8);
|
||||||
let text_container = if delegate {
|
// let text_container = if delegate {
|
||||||
// text widget based
|
// // text widget based
|
||||||
let font_size =
|
|
||||||
scale_font(slide.font_size() as f32, width);
|
|
||||||
let lines = slide_text.lines();
|
|
||||||
let text: Vec<Element<Message>> = lines
|
|
||||||
.map(|t| {
|
|
||||||
rich_text::<
|
|
||||||
'_,
|
|
||||||
&str,
|
|
||||||
Message,
|
|
||||||
iced::Theme,
|
|
||||||
iced::Renderer,
|
|
||||||
>([span(format!("{}\n", t))
|
|
||||||
.background(
|
|
||||||
Background::Color(Color::BLACK)
|
|
||||||
.scale_alpha(0.4),
|
|
||||||
)
|
|
||||||
.border(border::rounded(10))
|
|
||||||
.padding(10)])
|
|
||||||
.size(font_size)
|
|
||||||
.font(font)
|
|
||||||
.center()
|
|
||||||
.into()
|
|
||||||
// let chars: Vec<Span> = t
|
|
||||||
// .chars()
|
|
||||||
// .map(|c| -> Span {
|
|
||||||
// let character: String = format!("{}/n", c);
|
|
||||||
// span(character)
|
|
||||||
// .size(font_size)
|
|
||||||
// .font(font)
|
|
||||||
// .background(
|
|
||||||
// Background::Color(Color::BLACK)
|
|
||||||
// .scale_alpha(0.4),
|
|
||||||
// )
|
|
||||||
// .border(border::rounded(10))
|
|
||||||
// .padding(10)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
let text = Column::with_children(text).spacing(26);
|
|
||||||
Container::new(text)
|
|
||||||
.center(Length::Fill)
|
|
||||||
.align_x(Horizontal::Left)
|
|
||||||
} else {
|
|
||||||
// SVG based
|
|
||||||
let text = slide.text_svg.view().map(|m| Message::None);
|
|
||||||
Container::new(text)
|
|
||||||
.center(Length::Fill)
|
|
||||||
.align_x(Horizontal::Left)
|
|
||||||
// text widget based
|
|
||||||
// let font_size =
|
// let font_size =
|
||||||
// scale_font(slide.font_size() as f32, width);
|
// scale_font(slide.font_size() as f32, width);
|
||||||
// let lines = slide_text.lines();
|
// let lines = slide_text.lines();
|
||||||
|
@ -787,7 +744,63 @@ pub(crate) fn slide_view(
|
||||||
// Container::new(text)
|
// Container::new(text)
|
||||||
// .center(Length::Fill)
|
// .center(Length::Fill)
|
||||||
// .align_x(Horizontal::Left)
|
// .align_x(Horizontal::Left)
|
||||||
};
|
// } else {
|
||||||
|
// // SVG based
|
||||||
|
// let text: Element<Message> =
|
||||||
|
// if let Some(text) = &slide.text_svg {
|
||||||
|
// if let Some(handle) = &text.handle {
|
||||||
|
// debug!("we made it boys");
|
||||||
|
// Image::new(handle)
|
||||||
|
// .content_fit(ContentFit::Cover)
|
||||||
|
// .width(Length::Fill)
|
||||||
|
// .height(Length::Fill)
|
||||||
|
// .into()
|
||||||
|
// } else {
|
||||||
|
// Space::with_width(0).into()
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// Space::with_width(0).into()
|
||||||
|
// };
|
||||||
|
// Container::new(text)
|
||||||
|
// .center(Length::Fill)
|
||||||
|
// .align_x(Horizontal::Left)
|
||||||
|
// // text widget based
|
||||||
|
// // let font_size =
|
||||||
|
// // scale_font(slide.font_size() as f32, width);
|
||||||
|
// // let lines = slide_text.lines();
|
||||||
|
// // let text: Vec<Element<Message>> = lines
|
||||||
|
// // .map(|t| {
|
||||||
|
// // rich_text([span(format!("{}\n", t))
|
||||||
|
// // .background(
|
||||||
|
// // Background::Color(Color::BLACK)
|
||||||
|
// // .scale_alpha(0.4),
|
||||||
|
// // )
|
||||||
|
// // .border(border::rounded(10))
|
||||||
|
// // .padding(10)])
|
||||||
|
// // .size(font_size)
|
||||||
|
// // .font(font)
|
||||||
|
// // .center()
|
||||||
|
// // .into()
|
||||||
|
// // // let chars: Vec<Span> = t
|
||||||
|
// // // .chars()
|
||||||
|
// // // .map(|c| -> Span {
|
||||||
|
// // // let character: String = format!("{}/n", c);
|
||||||
|
// // // span(character)
|
||||||
|
// // // .size(font_size)
|
||||||
|
// // // .font(font)
|
||||||
|
// // // .background(
|
||||||
|
// // // Background::Color(Color::BLACK)
|
||||||
|
// // // .scale_alpha(0.4),
|
||||||
|
// // // )
|
||||||
|
// // // .border(border::rounded(10))
|
||||||
|
// // // .padding(10)
|
||||||
|
// // })
|
||||||
|
// // .collect();
|
||||||
|
// // let text = Column::with_children(text).spacing(26);
|
||||||
|
// // Container::new(text)
|
||||||
|
// // .center(Length::Fill)
|
||||||
|
// // .align_x(Horizontal::Left)
|
||||||
|
// };
|
||||||
|
|
||||||
// let stroke_text_container = Container::new(stroke_text)
|
// let stroke_text_container = Container::new(stroke_text)
|
||||||
// .center(Length::Fill)
|
// .center(Length::Fill)
|
||||||
|
@ -795,6 +808,21 @@ pub(crate) fn slide_view(
|
||||||
|
|
||||||
// let text_stack =
|
// let text_stack =
|
||||||
// stack!(stroke_text_container, text_container);
|
// stack!(stroke_text_container, text_container);
|
||||||
|
|
||||||
|
let text: Element<Message> =
|
||||||
|
if let Some(text) = &slide.text_svg {
|
||||||
|
if let Some(handle) = &text.handle {
|
||||||
|
image(handle)
|
||||||
|
.content_fit(ContentFit::Cover)
|
||||||
|
.width(width)
|
||||||
|
.height(size.height)
|
||||||
|
.into()
|
||||||
|
} else {
|
||||||
|
Space::with_width(0).into()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Space::with_width(0).into()
|
||||||
|
};
|
||||||
let black = Container::new(Space::new(0, 0))
|
let black = Container::new(Space::new(0, 0))
|
||||||
.style(|_| {
|
.style(|_| {
|
||||||
container::background(Background::Color(Color::BLACK))
|
container::background(Background::Color(Color::BLACK))
|
||||||
|
@ -802,7 +830,7 @@ pub(crate) fn slide_view(
|
||||||
.clip(true)
|
.clip(true)
|
||||||
.width(width)
|
.width(width)
|
||||||
.height(size.height);
|
.height(size.height);
|
||||||
let container = match slide.background().kind {
|
let background = match slide.background().kind {
|
||||||
BackgroundKind::Image => {
|
BackgroundKind::Image => {
|
||||||
let path = slide.background().path.clone();
|
let path = slide.background().path.clone();
|
||||||
Container::new(
|
Container::new(
|
||||||
|
@ -829,15 +857,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())
|
||||||
})
|
})
|
||||||
|
@ -851,11 +879,8 @@ pub(crate) fn slide_view(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let stack = stack!(
|
let stack =
|
||||||
black,
|
stack!(black, background.center(Length::Fill), text);
|
||||||
container.center(Length::Fill),
|
|
||||||
text_container
|
|
||||||
);
|
|
||||||
Container::new(stack).center(Length::Fill).into()
|
Container::new(stack).center(Length::Fill).into()
|
||||||
});
|
});
|
||||||
// let vid = if let Some(video) = &video {
|
// let vid = if let Some(video) = &video {
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
use std::{io, path::PathBuf};
|
use std::{io, path::PathBuf};
|
||||||
|
|
||||||
use iced::{
|
use cosmic::{
|
||||||
|
iced::{Color, Font, Length, Size},
|
||||||
|
prelude::*,
|
||||||
widget::{
|
widget::{
|
||||||
self,
|
self,
|
||||||
canvas::{self, Program, Stroke},
|
canvas::{self, Program, Stroke},
|
||||||
container, Canvas,
|
container, Canvas,
|
||||||
},
|
},
|
||||||
Color, Font, Length, Renderer, Size,
|
Renderer,
|
||||||
};
|
};
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
||||||
|
@ -49,14 +51,14 @@ pub enum SlideError {
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
struct EditorProgram {
|
struct EditorProgram {
|
||||||
mouse_button_pressed: Option<iced::mouse::Button>,
|
mouse_button_pressed: Option<cosmic::iced::mouse::Button>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SlideEditor {
|
impl SlideEditor {
|
||||||
pub fn view<'a>(
|
pub fn view<'a>(
|
||||||
&'a self,
|
&'a self,
|
||||||
font: Font,
|
font: Font,
|
||||||
) -> iced::Element<'a, SlideWidget> {
|
) -> cosmic::Element<'a, SlideWidget> {
|
||||||
container(
|
container(
|
||||||
widget::canvas(&self.program)
|
widget::canvas(&self.program)
|
||||||
.height(Length::Fill)
|
.height(Length::Fill)
|
||||||
|
@ -66,9 +68,9 @@ impl SlideEditor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ensure to use the `iced::Theme and iced::Renderer` here
|
/// Ensure to use the `cosmic::Theme and cosmic::Renderer` here
|
||||||
/// or else it will not compile
|
/// or else it will not compile
|
||||||
impl<'a> Program<SlideWidget, iced::Theme, iced::Renderer>
|
impl<'a> Program<SlideWidget, cosmic::Theme, cosmic::Renderer>
|
||||||
for EditorProgram
|
for EditorProgram
|
||||||
{
|
{
|
||||||
type State = ();
|
type State = ();
|
||||||
|
@ -77,9 +79,9 @@ impl<'a> Program<SlideWidget, iced::Theme, iced::Renderer>
|
||||||
&self,
|
&self,
|
||||||
state: &Self::State,
|
state: &Self::State,
|
||||||
renderer: &Renderer,
|
renderer: &Renderer,
|
||||||
theme: &iced::Theme,
|
theme: &cosmic::Theme,
|
||||||
bounds: iced::Rectangle,
|
bounds: cosmic::iced::Rectangle,
|
||||||
cursor: iced::mouse::Cursor,
|
cursor: cosmic::iced_core::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 +90,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::Point { x: 10.0, y: 10.0 },
|
cosmic::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,19 +116,21 @@ impl<'a> Program<SlideWidget, iced::Theme, iced::Renderer>
|
||||||
fn update(
|
fn update(
|
||||||
&self,
|
&self,
|
||||||
_state: &mut Self::State,
|
_state: &mut Self::State,
|
||||||
event: &iced::Event,
|
event: canvas::Event,
|
||||||
bounds: iced::Rectangle,
|
bounds: cosmic::iced::Rectangle,
|
||||||
_cursor: iced::mouse::Cursor,
|
_cursor: cosmic::iced_core::mouse::Cursor,
|
||||||
) -> std::option::Option<iced::widget::Action<SlideWidget>> {
|
) -> (canvas::event::Status, Option<SlideWidget>) {
|
||||||
match event {
|
match event {
|
||||||
iced::Event::Mouse(event) => match event {
|
canvas::Event::Mouse(event) => match event {
|
||||||
iced::mouse::Event::CursorEntered => {
|
cosmic::iced::mouse::Event::CursorEntered => {
|
||||||
debug!("cursor entered")
|
debug!("cursor entered")
|
||||||
}
|
}
|
||||||
iced::mouse::Event::CursorLeft => {
|
cosmic::iced::mouse::Event::CursorLeft => {
|
||||||
debug!("cursor left")
|
debug!("cursor left")
|
||||||
}
|
}
|
||||||
iced::mouse::Event::CursorMoved { position } => {
|
cosmic::iced::mouse::Event::CursorMoved {
|
||||||
|
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
|
||||||
|
@ -135,34 +139,29 @@ impl<'a> Program<SlideWidget, iced::Theme, iced::Renderer>
|
||||||
debug!(?position, "cursor moved");
|
debug!(?position, "cursor moved");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
iced::mouse::Event::ButtonPressed(button) => {
|
cosmic::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::mouse::Event::ButtonReleased(button) => {
|
cosmic::iced::mouse::Event::ButtonReleased(
|
||||||
debug!(?button, "mouse button released")
|
button,
|
||||||
}
|
) => debug!(?button, "mouse button released"),
|
||||||
iced::mouse::Event::WheelScrolled { delta } => {
|
cosmic::iced::mouse::Event::WheelScrolled {
|
||||||
debug!(?delta, "scroll wheel")
|
delta,
|
||||||
}
|
} => debug!(?delta, "scroll wheel"),
|
||||||
},
|
},
|
||||||
iced::Event::Touch(event) => debug!("test"),
|
canvas::Event::Touch(event) => debug!("test"),
|
||||||
iced::Event::Keyboard(event) => debug!("test"),
|
canvas::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!(),
|
|
||||||
}
|
}
|
||||||
None
|
(canvas::event::Status::Ignored, None)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mouse_interaction(
|
fn mouse_interaction(
|
||||||
&self,
|
&self,
|
||||||
_state: &Self::State,
|
_state: &Self::State,
|
||||||
_bounds: iced::Rectangle,
|
_bounds: cosmic::iced::Rectangle,
|
||||||
_cursor: iced::mouse::Cursor,
|
_cursor: cosmic::iced_core::mouse::Cursor,
|
||||||
) -> iced::mouse::Interaction {
|
) -> cosmic::iced_core::mouse::Interaction {
|
||||||
iced::mouse::Interaction::default()
|
cosmic::iced_core::mouse::Interaction::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,25 +1,27 @@
|
||||||
use std::{io, path::PathBuf};
|
use std::{io, path::PathBuf, sync::Arc};
|
||||||
|
|
||||||
use dirs::font_dir;
|
use cosmic::{
|
||||||
use iced::{
|
dialog::file_chooser::open::Dialog,
|
||||||
advanced::graphics::text::cosmic_text::fontdb,
|
iced::{
|
||||||
font::{Family, Stretch, Style, Weight},
|
font::{Family, Stretch, Style, Weight},
|
||||||
widget::{
|
Font, Length,
|
||||||
button, column, combo_box, container, horizontal_space, row,
|
|
||||||
scrollable, text, text_editor, text_input, tooltip,
|
|
||||||
TextInput,
|
|
||||||
},
|
},
|
||||||
Element, Font, Length, Task,
|
iced_wgpu::graphics::text::cosmic_text::fontdb,
|
||||||
|
iced_widget::row,
|
||||||
|
theme,
|
||||||
|
widget::{
|
||||||
|
button, column, combo_box, container, horizontal_space, icon,
|
||||||
|
scrollable, text, text_editor, text_input,
|
||||||
|
},
|
||||||
|
Element, Task,
|
||||||
};
|
};
|
||||||
|
use dirs::font_dir;
|
||||||
use iced_video_player::Video;
|
use iced_video_player::Video;
|
||||||
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::{
|
ui::slide_editor::{self, SlideEditor},
|
||||||
slide_editor::{self, SlideEditor},
|
|
||||||
widgets::icon,
|
|
||||||
},
|
|
||||||
Background, BackgroundKind,
|
Background, BackgroundKind,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -29,7 +31,7 @@ use super::presenter::slide_view;
|
||||||
pub struct SongEditor {
|
pub struct SongEditor {
|
||||||
pub song: Option<Song>,
|
pub song: Option<Song>,
|
||||||
title: String,
|
title: String,
|
||||||
font_db: fontdb::Database,
|
font_db: Arc<fontdb::Database>,
|
||||||
fonts: Vec<(fontdb::ID, String)>,
|
fonts: Vec<(fontdb::ID, String)>,
|
||||||
fonts_combo: combo_box::State<String>,
|
fonts_combo: combo_box::State<String>,
|
||||||
font_sizes: combo_box::State<String>,
|
font_sizes: combo_box::State<String>,
|
||||||
|
@ -70,11 +72,9 @@ pub enum Message {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SongEditor {
|
impl SongEditor {
|
||||||
pub fn new() -> Self {
|
pub fn new(font_db: Arc<fontdb::Database>) -> Self {
|
||||||
let fonts = font_dir();
|
let fonts = font_dir();
|
||||||
debug!(?fonts);
|
debug!(?fonts);
|
||||||
let mut font_db = fontdb::Database::new();
|
|
||||||
font_db.load_system_fonts();
|
|
||||||
let mut fonts: Vec<(fontdb::ID, String)> = font_db
|
let mut fonts: Vec<(fontdb::ID, String)> = font_db
|
||||||
.faces()
|
.faces()
|
||||||
.map(|f| {
|
.map(|f| {
|
||||||
|
@ -131,7 +131,7 @@ impl SongEditor {
|
||||||
audio: PathBuf::new(),
|
audio: PathBuf::new(),
|
||||||
background: None,
|
background: None,
|
||||||
video: None,
|
video: None,
|
||||||
current_font: iced::font::Font::DEFAULT,
|
current_font: cosmic::font::default(),
|
||||||
ccli: "8".to_owned(),
|
ccli: "8".to_owned(),
|
||||||
slide_state: SlideEditor::default(),
|
slide_state: SlideEditor::default(),
|
||||||
}
|
}
|
||||||
|
@ -271,93 +271,100 @@ 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![
|
let column = column::with_children(vec![
|
||||||
self.toolbar(),
|
self.toolbar(),
|
||||||
row![
|
row![
|
||||||
container(self.left_column())
|
container(self.left_column())
|
||||||
.center_x(Length::FillPortion(2)),
|
.center_x(Length::FillPortion(2)),
|
||||||
container(slide_preview)
|
container(slide_preview)
|
||||||
.center_x(Length::FillPortion(3))
|
.center_x(Length::FillPortion(3))
|
||||||
],
|
|
||||||
]
|
]
|
||||||
.spacing(15);
|
.into(),
|
||||||
|
])
|
||||||
|
.spacing(theme::active().cosmic().space_l());
|
||||||
column.into()
|
column.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn slide_preview(&self) -> Element<Message> {
|
fn slide_preview(&self) -> Element<Message> {
|
||||||
if let Some(song) = &self.song {
|
// if let Some(song) = &self.song {
|
||||||
if let Ok(slides) = song.to_slides() {
|
// if let Ok(slides) = song.to_slides() {
|
||||||
let slides = slides
|
// let slides = slides
|
||||||
.iter()
|
// .iter()
|
||||||
.enumerate()
|
// .enumerate()
|
||||||
.map(|(index, slide)| {
|
// .map(|(index, slide)| {
|
||||||
container(
|
// container(
|
||||||
slide_view(
|
// slide_view(
|
||||||
slide.clone(),
|
// slide.clone(),
|
||||||
if index == 0 {
|
// if index == 0 {
|
||||||
&self.video
|
// &self.video
|
||||||
} else {
|
// } else {
|
||||||
&None
|
// &None
|
||||||
},
|
// },
|
||||||
self.current_font,
|
// self.current_font,
|
||||||
false,
|
// false,
|
||||||
false,
|
// false,
|
||||||
)
|
// )
|
||||||
.map(|_| Message::None),
|
// .map(|_| Message::None),
|
||||||
)
|
// )
|
||||||
.height(250)
|
// .height(250)
|
||||||
.center_x(Length::Fill)
|
// .center_x(Length::Fill)
|
||||||
.padding([0, 20])
|
// .padding([0, 20])
|
||||||
.clip(true)
|
// .clip(true)
|
||||||
.into()
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
scrollable(column(slides).spacing(20))
|
|
||||||
.height(Length::Fill)
|
|
||||||
.width(Length::Fill)
|
|
||||||
.into()
|
|
||||||
} else {
|
|
||||||
horizontal_space().into()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
horizontal_space().into()
|
|
||||||
}
|
|
||||||
// self.slide_state
|
|
||||||
// .view(Font::with_name("Quicksand Bold"))
|
|
||||||
// .map(|_s| Message::None)
|
|
||||||
// .into()
|
// .into()
|
||||||
|
// })
|
||||||
|
// .collect();
|
||||||
|
// scrollable(
|
||||||
|
// column::with_children(slides)
|
||||||
|
// .spacing(theme::active().cosmic().space_l()),
|
||||||
|
// )
|
||||||
|
// .height(Length::Fill)
|
||||||
|
// .width(Length::Fill)
|
||||||
|
// .into()
|
||||||
|
// } else {
|
||||||
|
// horizontal_space().into()
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// horizontal_space().into()
|
||||||
|
// }
|
||||||
|
self.slide_state
|
||||||
|
.view(Font::with_name("Quicksand Bold"))
|
||||||
|
.map(|_s| Message::None)
|
||||||
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn left_column(&self) -> Element<Message> {
|
fn left_column(&self) -> Element<Message> {
|
||||||
let title_input = text_input("song", self.title.as_ref())
|
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![
|
let lyric_input = column::with_children(vec![
|
||||||
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![
|
column::with_children(vec![
|
||||||
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()
|
||||||
|
@ -392,20 +399,16 @@ order",
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.width(200);
|
.width(theme::active().cosmic().space_xxl());
|
||||||
|
|
||||||
let background_selector = button(row!(
|
let background_selector = button::icon(
|
||||||
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,
|
||||||
|
@ -442,24 +445,25 @@ order",
|
||||||
|
|
||||||
impl Default for SongEditor {
|
impl Default for SongEditor {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::new()
|
let mut fontdb = fontdb::Database::new();
|
||||||
|
fontdb.load_system_fonts();
|
||||||
|
Self::new(Arc::new(fontdb))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn pick_background() -> Result<PathBuf, SongError> {
|
async fn pick_background() -> Result<PathBuf, SongError> {
|
||||||
// let dialog =
|
let dialog = Dialog::new().title("Choose a background...");
|
||||||
// AsyncFileDialog::new().set_title("Choose a background...");
|
dialog
|
||||||
// dialog
|
.open_file()
|
||||||
|
.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
|
||||||
// .map_err(|_| SongError::DialogClosed)
|
// .ok_or(SongError::DialogClosed)
|
||||||
// .map(|file| file.url().to_file_path().unwrap())
|
// .map(|file| file.path().to_owned())
|
||||||
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)]
|
||||||
|
|
|
@ -1,19 +1,29 @@
|
||||||
use std::{
|
use std::{
|
||||||
fmt::Display,
|
fmt::Display,
|
||||||
hash::{Hash, Hasher},
|
hash::{Hash, Hasher},
|
||||||
|
io::Read,
|
||||||
|
path::PathBuf,
|
||||||
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
use colors_transform::Rgb;
|
use colors_transform::Rgb;
|
||||||
use iced::{
|
use cosmic::{
|
||||||
|
iced::{
|
||||||
font::{Style, Weight},
|
font::{Style, Weight},
|
||||||
widget::{container, svg::Handle, Svg},
|
ContentFit, Length, Size,
|
||||||
Element, Length, Size,
|
},
|
||||||
|
prelude::*,
|
||||||
|
widget::{container, image::Handle, Image},
|
||||||
};
|
};
|
||||||
use tracing::error;
|
use resvg::{
|
||||||
|
tiny_skia::{self, Pixmap},
|
||||||
|
usvg::{fontdb, Tree},
|
||||||
|
};
|
||||||
|
use tracing::{debug, error};
|
||||||
|
|
||||||
use crate::TextAlignment;
|
use crate::TextAlignment;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, PartialEq)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub struct TextSvg {
|
pub struct TextSvg {
|
||||||
text: String,
|
text: String,
|
||||||
font: Font,
|
font: Font,
|
||||||
|
@ -21,7 +31,20 @@ pub struct TextSvg {
|
||||||
stroke: Option<Stroke>,
|
stroke: Option<Stroke>,
|
||||||
fill: Color,
|
fill: Color,
|
||||||
alignment: TextAlignment,
|
alignment: TextAlignment,
|
||||||
handle: Option<Handle>,
|
pub handle: Option<Handle>,
|
||||||
|
fontdb: Arc<resvg::usvg::fontdb::Database>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for TextSvg {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.text == other.text
|
||||||
|
&& self.font == other.font
|
||||||
|
&& self.shadow == other.shadow
|
||||||
|
&& self.stroke == other.stroke
|
||||||
|
&& self.fill == other.fill
|
||||||
|
&& self.alignment == other.alignment
|
||||||
|
&& self.handle == other.handle
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Hash for TextSvg {
|
impl Hash for TextSvg {
|
||||||
|
@ -43,11 +66,34 @@ pub struct Font {
|
||||||
size: u8,
|
size: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<iced::font::Font> for Font {
|
#[derive(Clone, Debug, Default, PartialEq, Hash)]
|
||||||
fn from(value: iced::font::Font) -> Self {
|
pub struct Shadow {
|
||||||
|
pub offset_x: i16,
|
||||||
|
pub offset_y: i16,
|
||||||
|
pub spread: u16,
|
||||||
|
pub color: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq, Hash)]
|
||||||
|
pub struct Stroke {
|
||||||
|
size: u16,
|
||||||
|
color: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Message {
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct Color(Rgb);
|
||||||
|
|
||||||
|
impl From<cosmic::font::Font> for Font {
|
||||||
|
fn from(value: cosmic::font::Font) -> Self {
|
||||||
Self {
|
Self {
|
||||||
name: match value.family {
|
name: match value.family {
|
||||||
iced::font::Family::Name(name) => name.to_string(),
|
cosmic::iced::font::Family::Name(name) => {
|
||||||
|
name.to_string()
|
||||||
|
}
|
||||||
_ => "Quicksand Bold".into(),
|
_ => "Quicksand Bold".into(),
|
||||||
},
|
},
|
||||||
size: 20,
|
size: 20,
|
||||||
|
@ -108,9 +154,6 @@ impl Font {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
|
||||||
pub struct Color(Rgb);
|
|
||||||
|
|
||||||
impl Hash for Color {
|
impl Hash for Color {
|
||||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
self.0.to_css_hex_string().hash(state);
|
self.0.to_css_hex_string().hash(state);
|
||||||
|
@ -153,24 +196,6 @@ impl Display for Color {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, PartialEq, Hash)]
|
|
||||||
pub struct Shadow {
|
|
||||||
pub offset_x: i16,
|
|
||||||
pub offset_y: i16,
|
|
||||||
pub spread: u16,
|
|
||||||
pub color: Color,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, PartialEq, Hash)]
|
|
||||||
pub struct Stroke {
|
|
||||||
size: u16,
|
|
||||||
color: Color,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum Message {
|
|
||||||
None,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TextSvg {
|
impl TextSvg {
|
||||||
pub fn new(text: impl Into<String>) -> Self {
|
pub fn new(text: impl Into<String>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
@ -206,12 +231,33 @@ impl TextSvg {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn fontdb(mut self, fontdb: Arc<fontdb::Database>) -> Self {
|
||||||
|
self.fontdb = fontdb;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn alignment(mut self, alignment: TextAlignment) -> Self {
|
pub fn alignment(mut self, alignment: TextAlignment) -> Self {
|
||||||
self.alignment = alignment;
|
self.alignment = alignment;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build(mut self) -> Self {
|
pub fn build(mut self) -> Self {
|
||||||
|
debug!("starting...");
|
||||||
|
|
||||||
|
let mut path = dirs::data_local_dir().unwrap();
|
||||||
|
path.push(PathBuf::from("lumina"));
|
||||||
|
path.push(PathBuf::from("temp"));
|
||||||
|
let file_title =
|
||||||
|
&self.text.lines().next().unwrap().trim_end();
|
||||||
|
path.push(PathBuf::from(file_title));
|
||||||
|
path.set_extension("png");
|
||||||
|
|
||||||
|
if path.exists() {
|
||||||
|
debug!("cached");
|
||||||
|
let handle = Handle::from_path(path);
|
||||||
|
self.handle = Some(handle);
|
||||||
|
return self;
|
||||||
|
}
|
||||||
let shadow = if let Some(shadow) = &self.shadow {
|
let shadow = if let Some(shadow) = &self.shadow {
|
||||||
format!("<filter id=\"shadow\"><feDropShadow dx=\"{}\" dy=\"{}\" stdDeviation=\"{}\" flood-color=\"{}\"/></filter>",
|
format!("<filter id=\"shadow\"><feDropShadow dx=\"{}\" dy=\"{}\" stdDeviation=\"{}\" flood-color=\"{}\"/></filter>",
|
||||||
shadow.offset_x,
|
shadow.offset_x,
|
||||||
|
@ -229,13 +275,13 @@ impl TextSvg {
|
||||||
} else {
|
} else {
|
||||||
"".into()
|
"".into()
|
||||||
};
|
};
|
||||||
let size = Size::new(640.0, 360.0);
|
let size = Size::new(1920.0, 1080.0);
|
||||||
|
let font_size = self.font.size as f32;
|
||||||
let total_lines = self.text.lines().count();
|
let total_lines = self.text.lines().count();
|
||||||
let half_lines = (total_lines / 2) as f32;
|
let half_lines = (total_lines / 2) as f32;
|
||||||
let middle_position = size.height / 2.0;
|
let middle_position = size.height / 2.0;
|
||||||
let line_spacing = 10.0;
|
let line_spacing = 10.0;
|
||||||
let text_and_line_spacing =
|
let text_and_line_spacing = font_size + line_spacing;
|
||||||
self.font.size as f32 + line_spacing;
|
|
||||||
let starting_y_position =
|
let starting_y_position =
|
||||||
middle_position - (half_lines * text_and_line_spacing);
|
middle_position - (half_lines * text_and_line_spacing);
|
||||||
|
|
||||||
|
@ -259,25 +305,35 @@ impl TextSvg {
|
||||||
size.height,
|
size.height,
|
||||||
shadow,
|
shadow,
|
||||||
self.font.name,
|
self.font.name,
|
||||||
self.font.size,
|
font_size,
|
||||||
self.fill, stroke, text);
|
self.fill, stroke, text);
|
||||||
let handle = Handle::from_memory(
|
debug!("text string built...");
|
||||||
Box::leak(
|
let resvg_tree = Tree::from_data(
|
||||||
<std::string::String as Clone>::clone(&final_svg)
|
&final_svg.as_bytes(),
|
||||||
.into_boxed_str(),
|
&resvg::usvg::Options {
|
||||||
|
fontdb: Arc::clone(&self.fontdb),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
)
|
)
|
||||||
.as_bytes(),
|
.expect("Woops mama");
|
||||||
);
|
debug!("parsed");
|
||||||
|
let transform = tiny_skia::Transform::default();
|
||||||
|
let mut pixmap =
|
||||||
|
Pixmap::new(size.width as u32, size.height as u32)
|
||||||
|
.expect("opops");
|
||||||
|
resvg::render(&resvg_tree, transform, &mut pixmap.as_mut());
|
||||||
|
let _ = pixmap.save_png(&path);
|
||||||
|
|
||||||
|
debug!("rendered");
|
||||||
|
let handle = Handle::from_path(path);
|
||||||
self.handle = Some(handle);
|
self.handle = Some(handle);
|
||||||
|
debug!("stored");
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn view<'a>(&self) -> Element<'a, Message> {
|
pub fn view<'a>(&self) -> Element<'a, Message> {
|
||||||
container(
|
Image::new(self.handle.clone().unwrap())
|
||||||
Svg::new(self.handle.clone().unwrap())
|
.content_fit(ContentFit::Cover)
|
||||||
.width(Length::Fill)
|
|
||||||
.height(Length::Fill),
|
|
||||||
)
|
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.height(Length::Fill)
|
.height(Length::Fill)
|
||||||
.into()
|
.into()
|
||||||
|
@ -317,6 +373,26 @@ pub fn color(color: impl AsRef<str>) -> Color {
|
||||||
Color::from_hex_str(color)
|
Color::from_hex_str(color)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn text_svg_generator(
|
||||||
|
slide: &mut crate::core::slide::Slide,
|
||||||
|
fontdb: Arc<fontdb::Database>,
|
||||||
|
) {
|
||||||
|
if slide.text().len() > 0 {
|
||||||
|
let text_svg = TextSvg::new(slide.text())
|
||||||
|
.alignment(slide.text_alignment())
|
||||||
|
.fill("#fff")
|
||||||
|
.shadow(shadow(2, 2, 5, "#000000"))
|
||||||
|
.stroke(stroke(3, "#000"))
|
||||||
|
.font(
|
||||||
|
Font::from(slide.font().clone())
|
||||||
|
.size(slide.font_size().try_into().unwrap()),
|
||||||
|
)
|
||||||
|
.fontdb(Arc::clone(&fontdb))
|
||||||
|
.build();
|
||||||
|
slide.text_svg = Some(text_svg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
|
|
@ -1,115 +0,0 @@
|
||||||
// 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)),
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,187 +0,0 @@
|
||||||
// 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 => {}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,165 +0,0 @@
|
||||||
// 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,2 +1 @@
|
||||||
// pub mod slide_text;
|
// pub mod slide_text;
|
||||||
pub mod icon;
|
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
|
use cosmic::iced::advanced::layout::{self, Layout};
|
||||||
|
use cosmic::iced::advanced::renderer;
|
||||||
|
use cosmic::iced::advanced::widget::{self, Widget};
|
||||||
|
use cosmic::iced::border;
|
||||||
|
use cosmic::iced::mouse;
|
||||||
|
use cosmic::iced::{Color, Element, Length, Rectangle, Size};
|
||||||
use femtovg::renderer::WGPURenderer;
|
use femtovg::renderer::WGPURenderer;
|
||||||
use femtovg::{Canvas, TextContext};
|
use femtovg::{Canvas, TextContext};
|
||||||
use iced::iced::advanced::layout::{self, Layout};
|
|
||||||
use iced::iced::advanced::renderer;
|
|
||||||
use iced::iced::advanced::widget::{self, Widget};
|
|
||||||
use iced::iced::border;
|
|
||||||
use iced::iced::mouse;
|
|
||||||
use iced::iced::{Color, Element, Length, Rectangle, Size};
|
|
||||||
|
|
||||||
pub struct SlideText {
|
pub struct SlideText {
|
||||||
text: String,
|
text: String,
|
||||||
|
@ -23,7 +23,7 @@ impl SlideText {
|
||||||
});
|
});
|
||||||
let surface =
|
let surface =
|
||||||
instance.create_surface(window.clone()).unwrap();
|
instance.create_surface(window.clone()).unwrap();
|
||||||
let adapter = iced::iced::wgpu::util::initialize_adapter_from_env_or_default(&instance, Some(&surface))
|
let adapter = cosmic::iced::wgpu::util::initialize_adapter_from_env_or_default(&instance, Some(&surface))
|
||||||
.await
|
.await
|
||||||
.expect("Failed to find an appropriate adapter");
|
.expect("Failed to find an appropriate adapter");
|
||||||
let (device, queue) = adapter
|
let (device, queue) = adapter
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
(slide :background (image :source "~/pics/frodo.jpg" :fit fill)
|
(slide :background (image :source "~/pics/frodo.jpg" :fit fill)
|
||||||
(text "This is frodo" :font-size 90))
|
(text "This is frodo" :font-size 140))
|
||||||
(slide (video :source "~/vids/test/camprules2024.mp4" :fit contain))
|
(slide (video :source "~/vids/test/camprules2024.mp4" :fit contain))
|
||||||
(slide (video :source "~/vids/never give up.mkv" :fit contain))
|
(slide (video :source "~/vids/never give up.mkv" :fit contain))
|
||||||
(slide (video :source "~/vids/The promise of Rust.mkv" :fit contain))
|
(slide (video :source "~/vids/The promise of Rust.mkv" :fit contain))
|
||||||
(song :id 7 :author "North Point Worship"
|
(song :id 7 :author "North Point Worship"
|
||||||
:font "Quicksand Bold" :font-size 60
|
:font "Quicksand" :font-size 140
|
||||||
:shadow "" :stroke ""
|
:shadow "" :stroke ""
|
||||||
:title "Death Was Arrested"
|
:title "Death Was Arrested"
|
||||||
:background (image :source "file:///home/chris/nc/tfc/openlp/CMG - Bright Mountains 01.jpg" :fit cover)
|
:background (image :source "file:///home/chris/nc/tfc/openlp/CMG - Bright Mountains 01.jpg" :fit cover)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
(song :id 7 :author "North Point Worship"
|
(song :id 7 :author "North Point Worship"
|
||||||
:font "Quicksand Bold" :font-size 60
|
:font "Quicksand" :font-size 140
|
||||||
:title "Death Was Arrested"
|
:title "Death Was Arrested"
|
||||||
:background (image :source "~/nc/tfc/openlp/CMG - Bright Mountains 01.jpg" :fit cover)
|
:background (image :source "~/nc/tfc/openlp/CMG - Bright Mountains 01.jpg" :fit cover)
|
||||||
:text-alignment center
|
:text-alignment center
|
||||||
|
|
5
todo.org
5
todo.org
|
@ -17,6 +17,11 @@ Actually, what if we just made the svg at load/creation time and stored it in th
|
||||||
** SVG performs badly
|
** SVG performs badly
|
||||||
Since SVG's apparently run poorly in iced, instead I'll need to see about either creating a new text element, or teaching Iced to render strokes and shadows on text.
|
Since SVG's apparently run poorly in iced, instead I'll need to see about either creating a new text element, or teaching Iced to render strokes and shadows on text.
|
||||||
|
|
||||||
|
** Fork Cryoglyph
|
||||||
|
This fork will render text 3 times. Once for the text, once for the stroke, once for the shadow. This will only be used in the slides and therefore should not be much of a performance hit since we will only be render 3 copies of the given text. This should not be bad performance since it's not a large amount of text.
|
||||||
|
|
||||||
|
This also means in our custom widget with our custom fork, we can animate each individually perhaps.
|
||||||
|
|
||||||
* TODO [#C] Make the presenter more modular so things are easier to change.
|
* TODO [#C] Make the presenter more modular so things are easier to change.
|
||||||
|
|
||||||
* TODO Build library to see all available songs, images, videos, presentations, and slides
|
* TODO Build library to see all available songs, images, videos, presentations, and slides
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue