Compare commits

..

11 commits

Author SHA1 Message Date
fae83aedc5 trying to adjust the text_svg
Some checks are pending
/ test (push) Waiting to run
2025-09-05 15:26:14 -05:00
035f8896f1 updating to use the in memory rendered images for text
Some checks are pending
/ test (push) Waiting to run
2025-09-05 13:05:54 -05:00
6c8cb6c5b2 hey we are rasterizing right, just not loading into handle
Some checks failed
/ test (push) Has been cancelled
2025-09-03 15:49:35 -05:00
d6b4cc6297 some ui tweaks
Some checks are pending
/ test (push) Waiting to run
2025-09-02 15:27:38 -05:00
4792304d8b remove unused use statements
Some checks are pending
/ test (push) Waiting to run
2025-09-02 09:19:17 -05:00
23cd34388b update todo 2025-09-02 09:19:10 -05:00
4ccb186189 closer to using these text_svgs
Some checks failed
/ test (push) Has been cancelled
2025-08-29 16:41:24 -05:00
1446e35c58 trying to figure out a more performant way to do svgs
Some checks failed
/ test (push) Has been cancelled
2025-08-27 15:33:27 -05:00
5f3d867ad7 move the library to the left
Some checks are pending
/ test (push) Waiting to run
2025-08-27 13:06:20 -05:00
aa5e7420f1 moving service_list out of nav_bar
Some checks are pending
/ test (push) Waiting to run
2025-08-27 09:45:16 -05:00
213e47bf6d rearrange libcosmic dep for clarity 2025-08-27 09:45:00 -05:00
22 changed files with 2939 additions and 2138 deletions

2352
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

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

View file

@ -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};

View file

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

View file

@ -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,55 +632,22 @@ 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, font,
font, font_size,
font_size, text_alignment,
text_alignment, audio: self.audio,
audio: self.audio, video_loop,
video_loop, video_start_time,
video_start_time, video_end_time,
video_end_time, text_svg: self.text_svg,
text_svg, ..Default::default()
..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()
})
}
} }
} }
#[derive(Debug, Clone, Default)]
struct Image {
pub source: String,
pub fit: String,
pub children: Vec<String>,
}
impl Image { impl Image {
fn new() -> Self { fn new() -> Self {
Self { Self {

View file

@ -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};

View file

@ -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};

File diff suppressed because it is too large Load diff

View file

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

View file

@ -1,30 +1,30 @@
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}, presentations::{update_presentation_in_db, Presentation},
presentations::{update_presentation_in_db, Presentation}, 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,34 +373,65 @@ 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);
mouse_area(visual_item) DndSource::<Message, ServiceItem>::new(
// .on_drag(Message::DragItem( mouse_area(visual_item)
// service_item.clone(), .on_drag(Message::DragItem(service_item.clone()))
// )) .on_enter(Message::HoverItem(
.on_enter(Message::HoverItem(Some(( 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((
.on_exit(Message::HoverItem(None)) model.kind,
.on_press(Message::SelectItem(Some( index as i32,
(model.kind, index as i32), ))),
))) )
.into() .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()
}, },
) )
}) })
.spacing(2) .spacing(2)
.width(Length::Fill), .width(Length::Fill),
) )
.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 {
theme::active()
.cosmic()
.accent_text_color()
.into()
}
} else {
theme::active()
.cosmic()
.accent_text_color()
.into()
}
} else { } else {
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 {
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()
} }

View file

@ -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::{
alignment::Horizontal, iced::{
border, alignment::Horizontal,
font::{Family, Stretch, Style, Weight}, border,
widget::{ font::{Family, Stretch, Style, Weight},
container, image, mouse_area, responsive, rich_text, Background, Border, Color, ContentFit, Font, Length, Shadow,
scrollable::{ Vector,
self, scroll_to, AbsoluteOffset, Direction, Id, Scrollbar,
},
span, stack, text, vertical_rule, Column, Container, Row,
Space,
}, },
Background, Border, Color, ContentFit, Element, Font, Length, iced_widget::{
Shadow, Task, Vector, rich_text,
scrollable::{
scroll_to, AbsoluteOffset, Direction, Scrollbar,
},
span, stack, vertical_rule,
},
prelude::*,
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,16 +539,15 @@ 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) .id(self.scroll_id.clone());
.id(self.scroll_id.clone());
row.into() row.into()
} }
@ -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,92 +707,100 @@ 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 = // 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();
let text: Vec<Element<Message>> = lines // let text: Vec<Element<Message>> = lines
.map(|t| { // .map(|t| {
rich_text::< // rich_text([span(format!("{}\n", t))
'_, // .background(
&str, // Background::Color(Color::BLACK)
Message, // .scale_alpha(0.4),
iced::Theme, // )
iced::Renderer, // .border(border::rounded(10))
>([span(format!("{}\n", t)) // .padding(10)])
.background( // .size(font_size)
Background::Color(Color::BLACK) // .font(font)
.scale_alpha(0.4), // .center()
) // .into()
.border(border::rounded(10)) // // let chars: Vec<Span> = t
.padding(10)]) // // .chars()
.size(font_size) // // .map(|c| -> Span {
.font(font) // // let character: String = format!("{}/n", c);
.center() // // span(character)
.into() // // .size(font_size)
// let chars: Vec<Span> = t // // .font(font)
// .chars() // // .background(
// .map(|c| -> Span { // // Background::Color(Color::BLACK)
// let character: String = format!("{}/n", c); // // .scale_alpha(0.4),
// span(character) // // )
// .size(font_size) // // .border(border::rounded(10))
// .font(font) // // .padding(10)
// .background( // })
// Background::Color(Color::BLACK) // .collect();
// .scale_alpha(0.4), // let text = Column::with_children(text).spacing(26);
// ) // Container::new(text)
// .border(border::rounded(10)) // .center(Length::Fill)
// .padding(10) // .align_x(Horizontal::Left)
}) // } else {
.collect(); // // SVG based
let text = Column::with_children(text).spacing(26); // let text: Element<Message> =
Container::new(text) // if let Some(text) = &slide.text_svg {
.center(Length::Fill) // if let Some(handle) = &text.handle {
.align_x(Horizontal::Left) // debug!("we made it boys");
} else { // Image::new(handle)
// SVG based // .content_fit(ContentFit::Cover)
let text = slide.text_svg.view().map(|m| Message::None); // .width(Length::Fill)
Container::new(text) // .height(Length::Fill)
.center(Length::Fill) // .into()
.align_x(Horizontal::Left) // } else {
// text widget based // Space::with_width(0).into()
// let font_size = // }
// scale_font(slide.font_size() as f32, width); // } else {
// let lines = slide_text.lines(); // Space::with_width(0).into()
// let text: Vec<Element<Message>> = lines // };
// .map(|t| { // Container::new(text)
// rich_text([span(format!("{}\n", t)) // .center(Length::Fill)
// .background( // .align_x(Horizontal::Left)
// Background::Color(Color::BLACK) // // text widget based
// .scale_alpha(0.4), // // let font_size =
// ) // // scale_font(slide.font_size() as f32, width);
// .border(border::rounded(10)) // // let lines = slide_text.lines();
// .padding(10)]) // // let text: Vec<Element<Message>> = lines
// .size(font_size) // // .map(|t| {
// .font(font) // // rich_text([span(format!("{}\n", t))
// .center() // // .background(
// .into() // // Background::Color(Color::BLACK)
// // let chars: Vec<Span> = t // // .scale_alpha(0.4),
// // .chars() // // )
// // .map(|c| -> Span { // // .border(border::rounded(10))
// // let character: String = format!("{}/n", c); // // .padding(10)])
// // span(character) // // .size(font_size)
// // .size(font_size) // // .font(font)
// // .font(font) // // .center()
// // .background( // // .into()
// // Background::Color(Color::BLACK) // // // let chars: Vec<Span> = t
// // .scale_alpha(0.4), // // // .chars()
// // ) // // // .map(|c| -> Span {
// // .border(border::rounded(10)) // // // let character: String = format!("{}/n", c);
// // .padding(10) // // // span(character)
// }) // // // .size(font_size)
// .collect(); // // // .font(font)
// let text = Column::with_children(text).spacing(26); // // // .background(
// Container::new(text) // // // Background::Color(Color::BLACK)
// .center(Length::Fill) // // // .scale_alpha(0.4),
// .align_x(Horizontal::Left) // // // )
}; // // // .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 {

View file

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

View file

@ -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))
], ]
] .into(),
.spacing(15); ])
.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() // .into()
}) // })
.collect(); // .collect();
scrollable(column(slides).spacing(20)) // scrollable(
.height(Length::Fill) // column::with_children(slides)
.width(Length::Fill) // .spacing(theme::active().cosmic().space_l()),
.into() // )
} else { // .height(Length::Fill)
horizontal_space().into() // .width(Length::Fill)
} // .into()
} else { // } else {
horizontal_space().into() // horizontal_space().into()
} // }
// self.slide_state // } else {
// .view(Font::with_name("Quicksand Bold")) // horizontal_space().into()
// .map(|_s| Message::None) // }
// .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)]

View file

@ -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::{
font::{Style, Weight}, iced::{
widget::{container, svg::Handle, Svg}, font::{Style, Weight},
Element, Length, Size, ContentFit, 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,28 +305,38 @@ 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),
.as_bytes(), ..Default::default()
); },
)
.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) .width(Length::Fill)
.height(Length::Fill), .height(Length::Fill)
) .into()
.width(Length::Fill)
.height(Length::Fill)
.into()
} }
fn text_spans(&self) -> Vec<String> { fn text_spans(&self) -> Vec<String> {
@ -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;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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