Compare commits

..

4 commits

Author SHA1 Message Date
80a9b48ae9 my fork of iced_video_player
Some checks failed
/ test (push) Has been cancelled
2025-08-27 09:02:46 -05:00
1861f357a8 well crap
Some checks are pending
/ test (push) Waiting to run
2025-08-26 15:25:04 -05:00
4ae6a9a9a7 ohh
Some checks are pending
/ test (push) Waiting to run
2025-08-26 09:47:35 -05:00
c886d18134 beginning the switch... lots to go yet
Some checks are pending
/ test (push) Waiting to run
2025-08-26 09:44:06 -05:00
22 changed files with 2137 additions and 2938 deletions

2350
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -8,6 +8,7 @@ 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"
@ -32,22 +33,23 @@ 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.libcosmic] [dependencies.iced]
git = "https://github.com/pop-os/libcosmic" git = "https://github.com/iced-rs/iced"
default-features = false branch = "master"
features = ["debug", "winit", "desktop", "winit_wgpu", "winit_tokio", "tokio", "rfd", "dbus-config", "a11y", "wgpu", "multi-window"] features = ["wgpu", "image", "advanced", "svg", "canvas", "hot", "debug", "lazy", "tokio"]
[dependencies.iced_video_player] [dependencies.iced_video_player]
git = "https://github.com/jackpot51/iced_video_player.git" git = "https://git.tfcconnection.org/chris/iced_video_player"
branch = "cosmic" branch = "master"
features = ["wgpu"] # branch = "cosmic"
# [profile.dev] # [profile.dev]
# opt-level = 3 # opt-level = 3

View file

@ -1,6 +1,5 @@
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,12 +1,11 @@
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, Mutex}; use std::sync::Arc;
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;
@ -18,13 +17,13 @@ use super::videos::Video;
use super::kinds::ServiceItemKind; use super::kinds::ServiceItemKind;
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, PartialEq, Clone)]
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: Vec<Slide>, pub slides: Arc<[Slide]>,
// pub item: Box<dyn ServiceTrait>, // pub item: Box<dyn ServiceTrait>,
} }
@ -57,29 +56,29 @@ impl TryFrom<(Vec<u8>, String)> for ServiceItem {
} }
} }
impl AllowedMimeTypes for ServiceItem { // impl AllowedMimeTypes for ServiceItem {
fn allowed() -> Cow<'static, [String]> { // fn allowed() -> Cow<'static, [String]> {
Cow::from(vec!["application/service-item".to_string()]) // Cow::from(vec!["application/service-item".to_string()])
} // }
} // }
impl AsMimeTypes for ServiceItem { // impl AsMimeTypes for ServiceItem {
fn available(&self) -> Cow<'static, [String]> { // fn available(&self) -> Cow<'static, [String]> {
debug!(?self); // debug!(?self);
Cow::from(vec!["application/service-item".to_string()]) // Cow::from(vec!["application/service-item".to_string()])
} // }
fn as_bytes( // fn as_bytes(
&self, // &self,
mime_type: &str, // mime_type: &str,
) -> Option<std::borrow::Cow<'static, [u8]>> { // ) -> Option<std::borrow::Cow<'static, [u8]>> {
debug!(?self); // debug!(?self);
debug!(mime_type); // debug!(mime_type);
let val = Value::from(self); // let val = Value::from(self);
let val = String::from(val); // let val = String::from(val);
Some(Cow::from(val.into_bytes())) // Some(Cow::from(val.into_bytes()))
} // }
} // }
impl From<&ServiceItem> for Value { impl From<&ServiceItem> for Value {
fn from(value: &ServiceItem) -> Self { fn from(value: &ServiceItem) -> Self {
@ -122,7 +121,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: vec![], slides: Arc::new([]),
// item: Box::new(Image::default()), // item: Box::new(Image::default()),
} }
} }
@ -172,7 +171,7 @@ impl From<&Value> for ServiceItem {
kind: ServiceItemKind::Content( kind: ServiceItemKind::Content(
slide.clone(), slide.clone(),
), ),
slides: vec![slide], slides: Arc::new([slide]),
} }
} else if let Some(background) = } else if let Some(background) =
list.get(background_pos) list.get(background_pos)

View file

@ -1,13 +1,11 @@
// use cosmic::dialog::ashpd::url::Url; // use iced::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;
@ -15,40 +13,6 @@ 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,
@ -239,6 +203,15 @@ 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" {
@ -249,6 +222,24 @@ 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()))])
@ -261,11 +252,6 @@ 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
@ -289,10 +275,6 @@ 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
} }
@ -632,6 +614,7 @@ 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,
@ -642,11 +625,43 @@ impl SlideBuilder {
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()
})
} 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 {
fn new() -> Self { fn new() -> Self {

View file

@ -1,6 +1,5 @@
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,7 +7,6 @@ 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 cosmic::iced::Length; use iced::Length;
struct DoubleSlider<'a, T, Message> { struct DoubleSlider<'a, T, Message> {
range: RangeInclusive<T>, range: RangeInclusive<T>,

View file

@ -1,23 +1,21 @@
use cosmic::{ use iced::{
iced::{ advanced::widget::{tree::State, Widget},
alignment::Vertical, clipboard::dnd::DndAction, alignment::Vertical,
futures::FutureExt, Background, Border, Color, Length, futures::FutureExt,
},
iced_core::widget::tree::State,
iced_widget::{column, row as rowm, text as textm},
theme, theme,
widget::{ widget::{
button, container, horizontal_space, icon, mouse_area, button, column, container, horizontal_space, mouse_area,
responsive, row, scrollable, text, text_input, Container, responsive, row, scrollable, text, text_input, Container,
DndSource, Space, Widget, Space,
}, },
Element, Task, Background, Border, Color, Element, Length, Task,
}; };
use miette::{IntoDiagnostic, Result}; use miette::{IntoDiagnostic, Result};
use sqlx::{pool::PoolConnection, Sqlite, SqlitePool}; use sqlx::{pool::PoolConnection, Sqlite, SqlitePool};
use tracing::{debug, error, warn}; use tracing::{debug, error, warn};
use crate::core::{ use crate::{
core::{
content::Content, content::Content,
images::{update_image_in_db, Image}, images::{update_image_in_db, Image},
model::{LibraryKind, Model}, model::{LibraryKind, Model},
@ -25,6 +23,8 @@ use crate::core::{
service_items::ServiceItem, service_items::ServiceItem,
songs::{update_song_in_db, Song}, songs::{update_song_in_db, Song},
videos::{update_video_in_db, Video}, videos::{update_video_in_db, Video},
},
ui::widgets::icon,
}; };
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -264,18 +264,12 @@ 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,
] ];
.height(Length::Fill) column.height(Length::Fill).spacing(5).into()
.padding(10)
.spacing(10)
.into();
column
} }
pub fn library_item<T>( pub fn library_item<T>(
@ -285,40 +279,40 @@ impl<'a> Library {
where where
T: Content, T: Content,
{ {
let mut row = row::<Message>().spacing(5); let mut row = row![].spacing(5);
match &model.kind { match &model.kind {
LibraryKind::Song => { LibraryKind::Song => {
row = row row = row
.push(icon::from_name("folder-music-symbolic")); .push(icon::from_name("folder-music-symbolic"));
row = row row =
.push(textm!("Songs").align_y(Vertical::Center)); row.push(text("Songs").align_y(Vertical::Center));
} }
LibraryKind::Video => { LibraryKind::Video => {
row = row row = row
.push(icon::from_name("folder-videos-symbolic")); .push(icon::from_name("folder-videos-symbolic"));
row = row row = row
.push(textm!("Videos").align_y(Vertical::Center)); .push(text("Videos").align_y(Vertical::Center));
} }
LibraryKind::Image => { LibraryKind::Image => {
row = row.push(icon::from_name( row = row.push(icon::from_name(
"folder-pictures-symbolic", "folder-pictures-symbolic",
)); ));
row = row row = row
.push(textm!("Images").align_y(Vertical::Center)); .push(text("Images").align_y(Vertical::Center));
} }
LibraryKind::Presentation => { LibraryKind::Presentation => {
row = row.push(icon::from_name( row = row.push(icon::from_name(
"x-office-presentation-symbolic", "x-office-presentation-symbolic",
)); ));
row = row.push( row = row.push(
textm!("Presentations").align_y(Vertical::Center), text("Presentations").align_y(Vertical::Center),
); );
} }
}; };
let item_count = model.items.len(); let item_count = model.items.len();
row = row.push(horizontal_space()); row = row.push(horizontal_space());
row = row row = row
.push(textm!("{}", item_count).align_y(Vertical::Center)); .push(text!("{}", item_count).align_y(Vertical::Center));
row = row.push( row = row.push(
icon::from_name({ icon::from_name({
if self.library_open == Some(model.kind) { if self.library_open == Some(model.kind) {
@ -338,19 +332,26 @@ impl<'a> Library {
match self.library_hovered { match self.library_hovered {
Some(lib) => Background::Color( Some(lib) => Background::Color(
if lib == model.kind { if lib == model.kind {
t.cosmic().button.hover.into() t.extended_palette()
.primary
.strong
.color
} else { } else {
t.cosmic().button.base.into() t.extended_palette()
.background
.base
.color
}, },
), ),
None => Background::Color( None => Background::Color(
t.cosmic().button.base.into(), t.extended_palette()
.background
.base
.color,
), ),
} }
}) })
.border(Border::default().rounded( .border(Border::default().rounded(5))
t.cosmic().corner_radii.radius_s,
))
}) })
.center_x(Length::Fill) .center_x(Length::Fill)
.center_y(Length::Shrink); .center_y(Length::Shrink);
@ -373,52 +374,21 @@ impl<'a> Library {
let visual_item = self let visual_item = self
.single_item(index, item, model) .single_item(index, item, model)
.map(|_| Message::None); .map(|_| Message::None);
DndSource::<Message, ServiceItem>::new(
mouse_area(visual_item) mouse_area(visual_item)
.on_drag(Message::DragItem(service_item.clone())) // .on_drag(Message::DragItem(
.on_enter(Message::HoverItem( // service_item.clone(),
Some(( // ))
.on_enter(Message::HoverItem(Some((
model.kind, model.kind,
index as i32, index as i32,
)), ))))
.on_double_click(Message::OpenItem(
Some((model.kind, index as i32)),
)) ))
.on_double_click(
Message::OpenItem(Some((
model.kind,
index as i32,
))),
)
.on_exit(Message::HoverItem(None)) .on_exit(Message::HoverItem(None))
.on_press(Message::SelectItem( .on_press(Message::SelectItem(Some(
Some(( (model.kind, index as i32),
model.kind, )))
index as i32,
)),
)),
)
.action(DndAction::Copy)
.drag_icon({
let model = model.kind;
move |i| {
let state = State::None;
let icon = match model {
LibraryKind::Song => icon::from_name(
"folder-music-symbolic",
).symbolic(true)
,
LibraryKind::Video => icon::from_name("folder-videos-symbolic"),
LibraryKind::Image => icon::from_name("folder-pictures-symbolic"),
LibraryKind::Presentation => icon::from_name("x-office-presentation-symbolic"),
};
(
icon.into(),
state,
i,
)
}})
.drag_content(move || {
service_item.to_owned()
})
.into() .into()
}, },
) )
@ -429,9 +399,9 @@ impl<'a> Library {
.spacing(5) .spacing(5)
.height(Length::Fill); .height(Length::Fill);
let library_toolbar = rowm!( let library_toolbar = row!(
text_input("Search...", ""), text_input("Search...", ""),
button::icon(icon::from_name("add")) button(icon::from_name("add"))
); );
let library_column = let library_column =
column![library_toolbar, items].spacing(3); column![library_toolbar, items].spacing(3);
@ -452,67 +422,36 @@ impl<'a> Library {
where where
T: Content, T: Content,
{ {
let text = Container::new(responsive(|size| { let item_text = Container::new(responsive(|size| {
text::heading(elide_text(item.title(), size.width)) text(elide_text(item.title(), size.width))
.center() .center()
.wrapping(textm::Wrapping::None) .wrapping(text::Wrapping::None)
.into() .into()
})) }))
.center_y(20) .center_y(20)
.center_x(Length::Fill); .center_x(Length::Fill);
let subtext = container(responsive(move |size| { let subtext = container(responsive(|size| {
let color: Color = if item.background().is_some() { if item.background().is_some() {
if let Some((library, selected)) = self.selected_item text(elide_text(item.subtext(), size.width))
{ .style(text::primary)
if model.kind == library
&& selected == index as i32
{
theme::active().cosmic().control_0().into()
} else {
theme::active()
.cosmic()
.accent_text_color()
.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() .center()
.wrapping(textm::Wrapping::None) .wrapping(text::Wrapping::None)
.class(color)
.into() .into()
} else {
text(elide_text(item.subtext(), size.width))
.style(text::primary)
.center()
.wrapping(text::Wrapping::None)
.into()
}
})) }))
.center_y(20) .center_y(20)
.center_x(Length::Fill); .center_x(Length::Fill);
let texts = column([text.into(), subtext.into()]); let texts = column([item_text.into(), subtext.into()]);
Container::new( Container::new(
rowm![horizontal_space().width(0), texts] row![horizontal_space().width(0), texts]
.spacing(10) .spacing(10)
.align_y(Vertical::Center), .align_y(Vertical::Center),
) )
@ -527,21 +466,9 @@ impl<'a> Library {
if model.kind == library if model.kind == library
&& selected == index as i32 && selected == index as i32
{ {
t.cosmic().accent.selected.into() t.extended_palette().primary.strong.color
} else { } else {
if let Some((library, hovered)) = t.extended_palette().primary.base.color
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
@ -549,20 +476,16 @@ impl<'a> Library {
if model.kind == library if model.kind == library
&& hovered == index as i32 && hovered == index as i32
{ {
t.cosmic().button.hover.into() t.extended_palette().primary.strong.color
} else { } else {
t.cosmic().button.base.into() t.extended_palette().primary.base.color
} }
} else { } else {
t.cosmic().button.base.into() t.extended_palette().background.strong.color
}, },
)) ))
.border( .border(Border::default().rounded(10))
Border::default()
.rounded(t.cosmic().corner_radii.radius_m),
)
}) })
.padding([3, 0])
.into() .into()
} }

View file

@ -1,36 +1,29 @@
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 cosmic::{ use iced::{
iced::{
alignment::Horizontal, alignment::Horizontal,
border, border,
font::{Family, Stretch, Style, Weight}, font::{Family, Stretch, Style, Weight},
Background, Border, Color, ContentFit, Font, Length, Shadow,
Vector,
},
iced_widget::{
rich_text,
scrollable::{
scroll_to, AbsoluteOffset, Direction, Scrollbar,
},
span, stack, vertical_rule,
},
prelude::*,
widget::{ widget::{
container, image, mouse_area, responsive, scrollable, text, container, image, mouse_area, responsive, rich_text,
Column, Container, Id, Image, Row, Space, scrollable::{
self, scroll_to, AbsoluteOffset, Direction, Id, Scrollbar,
}, },
Task, span, stack, text, vertical_rule, Column, Container, Row,
Space,
},
Background, Border, Color, ContentFit, Element, Font, Length,
Shadow, Task, Vector,
}; };
use iced_video_player::{gst_pbutils, Position, Video, VideoPlayer}; use iced_video_player::{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::text_svg, // ui::widgets::slide_text,
BackgroundKind, BackgroundKind,
}; };
@ -132,9 +125,7 @@ impl Presenter {
Some(v) Some(v)
} }
Err(e) => { Err(e) => {
error!( error!("Had an error creating the video object: {e}, likely the first slide isn't a video");
"Had an error creating the video object: {e}, likely the first slide isn't a video"
);
None None
} }
} }
@ -150,7 +141,6 @@ 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,
@ -171,7 +161,7 @@ impl Presenter {
) )
}, },
scroll_id: Id::unique(), scroll_id: Id::unique(),
current_font: cosmic::font::default(), current_font: iced::font::Font::DEFAULT,
} }
} }
@ -347,27 +337,27 @@ impl Presenter {
return Action::Task(Task::perform( return Action::Task(Task::perform(
async move { async move {
tokio::task::spawn_blocking(move || { tokio::task::spawn_blocking(move || {
match gst_pbutils::MissingPluginMessage::parse(&element) { // match gst_pbutils::MissingPluginMessage::parse(&element) {
Ok(missing_plugin) => { // Ok(missing_plugin) => {
let mut install_ctx = gst_pbutils::InstallPluginsContext::new(); // let mut install_ctx = gst_pbutils::InstallPluginsContext::new();
install_ctx // install_ctx
.set_desktop_id(&format!("{}.desktop", "org.chriscochrun.lumina")); // .set_desktop_id(&format!("{}.desktop", "org.chriscochrun.lumina"));
let install_detail = missing_plugin.installer_detail(); // let install_detail = missing_plugin.installer_detail();
println!("installing plugins: {}", install_detail); // println!("installing plugins: {}", install_detail);
let status = gst_pbutils::missing_plugins::install_plugins_sync( // let status = gst_pbutils::missing_plugins::install_plugins_sync(
&[&install_detail], // &[&install_detail],
Some(&install_ctx), // Some(&install_ctx),
); // );
info!("plugin install status: {}", status); // info!("plugin install status: {}", status);
info!( // info!(
"gstreamer registry update: {:?}", // "gstreamer registry update: {:?}",
gstreamer::Registry::update() // gstreamer::Registry::update()
); // );
} // }
Err(err) => { // Err(err) => {
warn!("failed to parse missing plugin message: {err}"); // warn!("failed to parse missing plugin message: {err}");
} // }
} // }
Message::None Message::None
}) })
.await .await
@ -380,7 +370,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();
@ -452,7 +442,7 @@ impl Presenter {
.style(move |t| { .style(move |t| {
let mut style = let mut style =
container::Style::default(); container::Style::default();
let theme = t.cosmic(); let theme = t;
let hovered = self.hovered_slide let hovered = self.hovered_slide
== Some(( == Some((
item_index, item_index,
@ -462,19 +452,25 @@ impl Presenter {
Some(Background::Color( Some(Background::Color(
if is_current_slide { if is_current_slide {
theme theme
.accent .extended_palette(
.base )
.into() .secondary
.strong
.color
} else if hovered { } else if hovered {
theme theme
.accent .extended_palette(
.hover )
.into() .secondary
.strong
.color
} else { } else {
theme theme
.palette .extended_palette(
.neutral_3 )
.into() .background
.neutral
.color
}, },
)); ));
style.border = Border::default() style.border = Border::default()
@ -507,7 +503,7 @@ impl Presenter {
.padding(10), .padding(10),
) )
.interaction( .interaction(
cosmic::iced::mouse::Interaction::Pointer, iced::mouse::Interaction::Pointer,
) )
.on_move(move |_| { .on_move(move |_| {
Message::HoveredSlide(Some(( Message::HoveredSlide(Some((
@ -525,11 +521,11 @@ impl Presenter {
let row = Row::from_vec(slides) let row = Row::from_vec(slides)
.spacing(10) .spacing(10)
.padding([20, 15]); .padding([20, 15]);
let label = text::body(item.title.clone()); let label = text(item.title.clone());
let label_container = container(label) let label_container = container(label)
.align_top(Length::Fill) .align_top(Length::Fill)
.align_left(Length::Fill) .align_left(Length::Fill)
.padding([0, 0, 0, 35]); .padding([0, 35]);
let divider = vertical_rule(2); let divider = vertical_rule(2);
items.push( items.push(
container(stack!(row, label_container)) container(stack!(row, label_container))
@ -539,11 +535,12 @@ impl Presenter {
items.push(divider.into()); items.push(divider.into());
}, },
); );
let row = let row = scrollable::Scrollable::new(
scrollable(container(Row::from_vec(items)).style(|t| { container(Row::from_vec(items)).style(|t| {
let style = container::Style::default(); let style = container::Style::default();
style.border(Border::default().width(2)) style.border(Border::default().width(2))
})) }),
)
.direction(Direction::Horizontal(Scrollbar::new())) .direction(Direction::Horizontal(Scrollbar::new()))
.height(Length::Fill) .height(Length::Fill)
.width(Length::Fill) .width(Length::Fill)
@ -581,7 +578,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.cosmic(); // let theme = t.iced();
// 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 {
@ -620,7 +617,7 @@ impl Presenter {
// .height(100) // .height(100)
// .padding(10), // .padding(10),
// ) // )
// .interaction(cosmic::iced::mouse::Interaction::Pointer) // .interaction(iced::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()));
@ -643,9 +640,7 @@ impl Presenter {
self.video = Some(v) self.video = Some(v)
} }
Err(e) => { Err(e) => {
error!( error!("Had an error creating the video object: {e}");
"Had an error creating the video object: {e}"
);
self.video = None; self.video = None;
} }
} }
@ -707,8 +702,56 @@ 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();
@ -744,63 +787,7 @@ 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)
@ -808,21 +795,6 @@ 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))
@ -830,7 +802,7 @@ pub(crate) fn slide_view(
.clip(true) .clip(true)
.width(width) .width(width)
.height(size.height); .height(size.height);
let background = match slide.background().kind { let container = match slide.background().kind {
BackgroundKind::Image => { BackgroundKind::Image => {
let path = slide.background().path.clone(); let path = slide.background().path.clone();
Container::new( Container::new(
@ -857,15 +829,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())
}) })
@ -879,8 +851,11 @@ pub(crate) fn slide_view(
} }
} }
}; };
let stack = let stack = stack!(
stack!(black, background.center(Length::Fill), text); black,
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,14 +1,12 @@
use std::{io, path::PathBuf}; use std::{io, path::PathBuf};
use cosmic::{ use iced::{
iced::{Color, Font, Length, Size},
prelude::*,
widget::{ widget::{
self, self,
canvas::{self, Program, Stroke}, canvas::{self, Program, Stroke},
container, Canvas, container, Canvas,
}, },
Renderer, Color, Font, Length, Renderer, Size,
}; };
use tracing::debug; use tracing::debug;
@ -51,14 +49,14 @@ pub enum SlideError {
#[derive(Debug, Default)] #[derive(Debug, Default)]
struct EditorProgram { struct EditorProgram {
mouse_button_pressed: Option<cosmic::iced::mouse::Button>, mouse_button_pressed: Option<iced::mouse::Button>,
} }
impl SlideEditor { impl SlideEditor {
pub fn view<'a>( pub fn view<'a>(
&'a self, &'a self,
font: Font, font: Font,
) -> cosmic::Element<'a, SlideWidget> { ) -> iced::Element<'a, SlideWidget> {
container( container(
widget::canvas(&self.program) widget::canvas(&self.program)
.height(Length::Fill) .height(Length::Fill)
@ -68,9 +66,9 @@ impl SlideEditor {
} }
} }
/// Ensure to use the `cosmic::Theme and cosmic::Renderer` here /// Ensure to use the `iced::Theme and iced::Renderer` here
/// or else it will not compile /// or else it will not compile
impl<'a> Program<SlideWidget, cosmic::Theme, cosmic::Renderer> impl<'a> Program<SlideWidget, iced::Theme, iced::Renderer>
for EditorProgram for EditorProgram
{ {
type State = (); type State = ();
@ -79,9 +77,9 @@ impl<'a> Program<SlideWidget, cosmic::Theme, cosmic::Renderer>
&self, &self,
state: &Self::State, state: &Self::State,
renderer: &Renderer, renderer: &Renderer,
theme: &cosmic::Theme, theme: &iced::Theme,
bounds: cosmic::iced::Rectangle, bounds: iced::Rectangle,
cursor: cosmic::iced_core::mouse::Cursor, cursor: iced::mouse::Cursor,
) -> Vec<canvas::Geometry<Renderer>> { ) -> Vec<canvas::Geometry<Renderer>> {
// We prepare a new `Frame` // We prepare a new `Frame`
let mut frame = canvas::Frame::new(renderer, bounds.size()); let mut frame = canvas::Frame::new(renderer, bounds.size());
@ -90,7 +88,7 @@ impl<'a> Program<SlideWidget, cosmic::Theme, cosmic::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(
cosmic::iced::Point { x: 10.0, y: 10.0 }, iced::Point { x: 10.0, y: 10.0 },
Size::new(frame_rect.width, frame_rect.height), Size::new(frame_rect.width, frame_rect.height),
); );
@ -116,21 +114,19 @@ impl<'a> Program<SlideWidget, cosmic::Theme, cosmic::Renderer>
fn update( fn update(
&self, &self,
_state: &mut Self::State, _state: &mut Self::State,
event: canvas::Event, event: &iced::Event,
bounds: cosmic::iced::Rectangle, bounds: iced::Rectangle,
_cursor: cosmic::iced_core::mouse::Cursor, _cursor: iced::mouse::Cursor,
) -> (canvas::event::Status, Option<SlideWidget>) { ) -> std::option::Option<iced::widget::Action<SlideWidget>> {
match event { match event {
canvas::Event::Mouse(event) => match event { iced::Event::Mouse(event) => match event {
cosmic::iced::mouse::Event::CursorEntered => { iced::mouse::Event::CursorEntered => {
debug!("cursor entered") debug!("cursor entered")
} }
cosmic::iced::mouse::Event::CursorLeft => { iced::mouse::Event::CursorLeft => {
debug!("cursor left") debug!("cursor left")
} }
cosmic::iced::mouse::Event::CursorMoved { iced::mouse::Event::CursorMoved { position } => {
position,
} => {
if bounds.x < position.x if bounds.x < position.x
&& bounds.y < position.y && bounds.y < position.y
&& (bounds.width + bounds.x) > position.x && (bounds.width + bounds.x) > position.x
@ -139,29 +135,34 @@ impl<'a> Program<SlideWidget, cosmic::Theme, cosmic::Renderer>
debug!(?position, "cursor moved"); debug!(?position, "cursor moved");
} }
} }
cosmic::iced::mouse::Event::ButtonPressed(button) => { iced::mouse::Event::ButtonPressed(button) => {
// self.mouse_button_pressed = Some(button); // self.mouse_button_pressed = Some(button);
debug!(?button, "mouse button pressed") debug!(?button, "mouse button pressed")
} }
cosmic::iced::mouse::Event::ButtonReleased( iced::mouse::Event::ButtonReleased(button) => {
button, debug!(?button, "mouse button released")
) => debug!(?button, "mouse button released"),
cosmic::iced::mouse::Event::WheelScrolled {
delta,
} => debug!(?delta, "scroll wheel"),
},
canvas::Event::Touch(event) => debug!("test"),
canvas::Event::Keyboard(event) => debug!("test"),
} }
(canvas::event::Status::Ignored, None) iced::mouse::Event::WheelScrolled { delta } => {
debug!(?delta, "scroll wheel")
}
},
iced::Event::Touch(event) => debug!("test"),
iced::Event::Keyboard(event) => debug!("test"),
iced::Event::Keyboard(event) => todo!(),
iced::Event::Mouse(event) => todo!(),
iced::Event::Window(event) => todo!(),
iced::Event::Touch(event) => todo!(),
iced::Event::InputMethod(event) => todo!(),
}
None
} }
fn mouse_interaction( fn mouse_interaction(
&self, &self,
_state: &Self::State, _state: &Self::State,
_bounds: cosmic::iced::Rectangle, _bounds: iced::Rectangle,
_cursor: cosmic::iced_core::mouse::Cursor, _cursor: iced::mouse::Cursor,
) -> cosmic::iced_core::mouse::Interaction { ) -> iced::mouse::Interaction {
cosmic::iced_core::mouse::Interaction::default() iced::mouse::Interaction::default()
} }
} }

View file

@ -1,27 +1,25 @@
use std::{io, path::PathBuf, sync::Arc}; use std::{io, path::PathBuf};
use cosmic::{
dialog::file_chooser::open::Dialog,
iced::{
font::{Family, Stretch, Style, Weight},
Font, Length,
},
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 dirs::font_dir;
use iced::{
advanced::graphics::text::cosmic_text::fontdb,
font::{Family, Stretch, Style, Weight},
widget::{
button, column, combo_box, container, horizontal_space, row,
scrollable, text, text_editor, text_input, tooltip,
TextInput,
},
Element, Font, Length, Task,
};
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::slide_editor::{self, SlideEditor}, ui::{
slide_editor::{self, SlideEditor},
widgets::icon,
},
Background, BackgroundKind, Background, BackgroundKind,
}; };
@ -31,7 +29,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: Arc<fontdb::Database>, font_db: 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>,
@ -72,9 +70,11 @@ pub enum Message {
} }
impl SongEditor { impl SongEditor {
pub fn new(font_db: Arc<fontdb::Database>) -> Self { pub fn new() -> 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: cosmic::font::default(), current_font: iced::font::Font::DEFAULT,
ccli: "8".to_owned(), ccli: "8".to_owned(),
slide_state: SlideEditor::default(), slide_state: SlideEditor::default(),
} }
@ -271,100 +271,93 @@ impl SongEditor {
let slide_preview = container(self.slide_preview()) let slide_preview = container(self.slide_preview())
.width(Length::FillPortion(2)); .width(Length::FillPortion(2));
let column = column::with_children(vec![ let column = column![
self.toolbar(), self.toolbar(),
row![ row![
container(self.left_column()) container(self.left_column())
.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()
// })
// .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() .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()
} }
fn left_column(&self) -> Element<Message> { fn left_column(&self) -> Element<Message> {
let title_input = text_input("song", &self.title) let title_input = text_input("song", self.title.as_ref())
.on_input(Message::ChangeTitle) .on_input(|_| Message::ChangeTitle);
.label("Song Title");
let author_input = text_input("author", &self.author) let author_input = text_input("author", &self.author)
.on_input(Message::ChangeAuthor) .on_input(|_| Message::ChangeAuthor);
.label("Song Author");
let verse_input = text_input( let verse_input = text_input(
"Verse "Verse
order", order",
&self.verse_order, &self.verse_order,
) )
.label("Verse Order")
.on_input(Message::ChangeVerseOrder); .on_input(Message::ChangeVerseOrder);
let lyric_title = text("Lyrics"); let lyric_title = text("Lyrics");
let lyric_input = column::with_children(vec![ let lyric_input = column![
lyric_title.into(), lyric_title.into(),
text_editor(&self.lyrics) text_editor(&self.lyrics)
.on_action(Message::ChangeLyrics) .on_action(Message::ChangeLyrics)
.height(Length::Fill) .height(Length::Fill)
.into(), .into(),
]) ]
.spacing(5); .spacing(5);
column::with_children(vec![ column![
title_input.into(), title_input.into(),
author_input.into(), author_input.into(),
verse_input.into(), verse_input.into(),
lyric_input.into(), lyric_input.into(),
]) ]
.spacing(25) .spacing(25)
.width(Length::FillPortion(2)) .width(Length::FillPortion(2))
.into() .into()
@ -399,16 +392,20 @@ order",
) )
}, },
) )
.width(theme::active().cosmic().space_xxl()); .width(200);
let background_selector = button::icon( let background_selector = button(row!(
icon::from_name("folder-pictures-symbolic").scale(2), icon::from_name("folder-pictures-symbolic").scale(2),
) "Background"
.label("Background") ))
.tooltip("Select an image or video background")
.on_press(Message::PickBackground) .on_press(Message::PickBackground)
.padding(10); .padding(10);
let background_selector = tooltip(
background_selector,
"Select an image or video background",
tooltip::Position::FollowCursor,
);
row![ row![
font_selector, font_selector,
font_size, font_size,
@ -445,25 +442,24 @@ order",
impl Default for SongEditor { impl Default for SongEditor {
fn default() -> Self { fn default() -> Self {
let mut fontdb = fontdb::Database::new(); Self::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 = Dialog::new().title("Choose a background..."); // let dialog =
dialog // AsyncFileDialog::new().set_title("Choose a background...");
.open_file() // dialog
.await
.map_err(|_| SongError::DialogClosed)
.map(|file| file.url().to_file_path().unwrap())
// rfd::AsyncFileDialog::new()
// .set_title("Choose a background...")
// .pick_file() // .pick_file()
// .await // .await
// .ok_or(SongError::DialogClosed) // .map_err(|_| SongError::DialogClosed)
// .map(|file| file.path().to_owned()) // .map(|file| file.url().to_file_path().unwrap())
rfd::AsyncFileDialog::new()
.set_title("Choose a background...")
.pick_file()
.await
.ok_or(SongError::DialogClosed)
.map(|file| file.path().to_owned())
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]

View file

@ -1,29 +1,19 @@
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 cosmic::{ use iced::{
iced::{
font::{Style, Weight}, font::{Style, Weight},
ContentFit, Length, Size, widget::{container, svg::Handle, Svg},
}, Element, Length, Size,
prelude::*,
widget::{container, image::Handle, Image},
}; };
use resvg::{ use tracing::error;
tiny_skia::{self, Pixmap},
usvg::{fontdb, Tree},
};
use tracing::{debug, error};
use crate::TextAlignment; use crate::TextAlignment;
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default, PartialEq)]
pub struct TextSvg { pub struct TextSvg {
text: String, text: String,
font: Font, font: Font,
@ -31,20 +21,7 @@ pub struct TextSvg {
stroke: Option<Stroke>, stroke: Option<Stroke>,
fill: Color, fill: Color,
alignment: TextAlignment, alignment: TextAlignment,
pub handle: Option<Handle>, 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 {
@ -66,34 +43,11 @@ pub struct Font {
size: u8, size: u8,
} }
#[derive(Clone, Debug, Default, PartialEq, Hash)] impl From<iced::font::Font> for Font {
pub struct Shadow { fn from(value: iced::font::Font) -> Self {
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 {
cosmic::iced::font::Family::Name(name) => { iced::font::Family::Name(name) => name.to_string(),
name.to_string()
}
_ => "Quicksand Bold".into(), _ => "Quicksand Bold".into(),
}, },
size: 20, size: 20,
@ -154,6 +108,9 @@ 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);
@ -196,6 +153,24 @@ 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 {
@ -231,33 +206,12 @@ 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,
@ -275,13 +229,13 @@ impl TextSvg {
} else { } else {
"".into() "".into()
}; };
let size = Size::new(1920.0, 1080.0); let size = Size::new(640.0, 360.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 = font_size + line_spacing; let text_and_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);
@ -305,35 +259,25 @@ impl TextSvg {
size.height, size.height,
shadow, shadow,
self.font.name, self.font.name,
font_size, self.font.size,
self.fill, stroke, text); self.fill, stroke, text);
debug!("text string built..."); let handle = Handle::from_memory(
let resvg_tree = Tree::from_data( Box::leak(
&final_svg.as_bytes(), <std::string::String as Clone>::clone(&final_svg)
&resvg::usvg::Options { .into_boxed_str(),
fontdb: Arc::clone(&self.fontdb),
..Default::default()
},
) )
.expect("Woops mama"); .as_bytes(),
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> {
Image::new(self.handle.clone().unwrap()) container(
.content_fit(ContentFit::Cover) Svg::new(self.handle.clone().unwrap())
.width(Length::Fill)
.height(Length::Fill),
)
.width(Length::Fill) .width(Length::Fill)
.height(Length::Fill) .height(Length::Fill)
.into() .into()
@ -373,26 +317,6 @@ 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

@ -0,0 +1,115 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
use super::{Icon, Named};
use iced::widget::{image, svg};
use std::borrow::Cow;
use std::ffi::OsStr;
use std::hash::Hash;
use std::path::PathBuf;
#[must_use]
#[derive(Clone, Debug, derive_setters::Setters)]
pub struct Handle {
pub symbolic: bool,
#[setters(skip)]
pub data: Data,
}
impl Handle {
#[inline]
pub fn icon(self) -> Icon {
super::icon(self)
}
}
#[must_use]
#[derive(Clone, Debug)]
pub enum Data {
Name(Named),
Image(image::Handle),
Svg(svg::Handle),
}
/// Create an icon handle from its path.
pub fn from_path(path: PathBuf) -> Handle {
Handle {
symbolic: path
.file_stem()
.and_then(OsStr::to_str)
.is_some_and(|name| name.ends_with("-symbolic")),
data: if path
.extension()
.is_some_and(|ext| ext == OsStr::new("svg"))
{
Data::Svg(svg::Handle::from_path(path))
} else {
Data::Image(image::Handle::from_path(path))
},
}
}
/// Create an image handle from memory.
pub fn from_raster_bytes(
bytes: impl Into<Cow<'static, [u8]>>
+ std::convert::AsRef<[u8]>
+ std::marker::Send
+ std::marker::Sync
+ 'static,
) -> Handle {
fn inner(bytes: Cow<'static, [u8]>) -> Handle {
Handle {
symbolic: false,
data: match bytes {
Cow::Owned(b) => {
Data::Image(image::Handle::from_bytes(b))
}
Cow::Borrowed(b) => {
Data::Image(image::Handle::from_bytes(b))
}
},
}
}
inner(bytes.into())
}
/// Create an image handle from RGBA data, where you must define the width and height.
pub fn from_raster_pixels(
width: u32,
height: u32,
pixels: impl Into<Cow<'static, [u8]>>
+ std::convert::AsRef<[u8]>
+ std::marker::Send
+ std::marker::Sync,
) -> Handle {
fn inner(
width: u32,
height: u32,
pixels: Cow<'static, [u8]>,
) -> Handle {
Handle {
symbolic: false,
data: match pixels {
Cow::Owned(pixels) => Data::Image(
image::Handle::from_rgba(width, height, pixels),
),
Cow::Borrowed(pixels) => Data::Image(
image::Handle::from_rgba(width, height, pixels),
),
},
}
}
inner(width, height, pixels.into())
}
/// Create a SVG handle from memory.
pub fn from_svg_bytes(
bytes: impl Into<Cow<'static, [u8]>>,
) -> Handle {
Handle {
symbolic: false,
data: Data::Svg(svg::Handle::from_memory(bytes)),
}
}

187
src/ui/widgets/icon/mod.rs Normal file
View file

@ -0,0 +1,187 @@
// Copyright 2022 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
//! Lazily-generated SVG icon widget for Iced.
mod named;
use std::ffi::OsStr;
use std::sync::Arc;
pub use named::{IconFallback, Named};
mod handle;
pub use handle::{
from_path, from_raster_bytes, from_raster_pixels, from_svg_bytes,
Data, Handle,
};
use derive_setters::Setters;
use iced::advanced::{image, svg};
use iced::widget::{Image, Svg};
use iced::Element;
use iced::Rotation;
use iced::{ContentFit, Length, Rectangle};
/// Create an [`Icon`] from a pre-existing [`Handle`]
pub fn icon(handle: Handle) -> Icon {
Icon {
content_fit: ContentFit::Fill,
handle,
height: None,
size: 16,
rotation: None,
width: None,
}
}
/// Create an icon handle from its XDG icon name.
pub fn from_name(name: impl Into<Arc<str>>) -> Named {
Named::new(name)
}
/// An image which may be an SVG or PNG.
#[must_use]
#[derive(Clone, Setters)]
pub struct Icon {
#[setters(skip)]
handle: Handle,
pub(super) size: u16,
content_fit: ContentFit,
#[setters(strip_option)]
width: Option<Length>,
#[setters(strip_option)]
height: Option<Length>,
#[setters(strip_option)]
rotation: Option<Rotation>,
}
impl Icon {
#[must_use]
pub fn into_svg_handle(
self,
) -> Option<iced::widget::svg::Handle> {
match self.handle.data {
Data::Name(named) => {
if let Some(path) = named.path() {
if path
.extension()
.is_some_and(|ext| ext == OsStr::new("svg"))
{
return Some(
iced::advanced::svg::Handle::from_path(
path,
),
);
}
}
}
Data::Image(_) => (),
Data::Svg(handle) => return Some(handle),
}
None
}
#[must_use]
fn view<'a, Message: 'a>(self) -> Element<'a, Message> {
let from_image = |handle| {
Image::new(handle)
.width(self.width.unwrap_or_else(|| {
Length::Fixed(f32::from(self.size))
}))
.height(self.height.unwrap_or_else(|| {
Length::Fixed(f32::from(self.size))
}))
.rotation(self.rotation.unwrap_or_default())
.content_fit(self.content_fit)
.into()
};
let from_svg = |handle| {
Svg::<crate::Theme>::new(handle)
.width(self.width.unwrap_or_else(|| {
Length::Fixed(f32::from(self.size))
}))
.height(self.height.unwrap_or_else(|| {
Length::Fixed(f32::from(self.size))
}))
.rotation(self.rotation.unwrap_or_default())
.content_fit(self.content_fit)
.into()
};
match self.handle.data {
Data::Name(named) => {
if let Some(path) = named.path() {
if path
.extension()
.is_some_and(|ext| ext == OsStr::new("svg"))
{
from_svg(svg::Handle::from_path(path))
} else {
from_image(image::Handle::from_path(path))
}
} else {
let bytes: &'static [u8] = &[];
from_svg(svg::Handle::from_memory(bytes))
}
}
Data::Image(handle) => from_image(handle),
Data::Svg(handle) => from_svg(handle),
}
}
}
impl<'a, Message: 'a> From<Icon> for Element<'a, Message> {
fn from(icon: Icon) -> Self {
icon.view::<Message>()
}
}
/// Draw an icon in the given bounds via the runtime's renderer.
pub fn draw(
renderer: &mut iced::Renderer,
handle: &Handle,
icon_bounds: Rectangle,
) {
enum IcedHandle {
Svg(svg::Handle),
Image(image::Handle),
}
let iced_handle = match handle.clone().data {
Data::Name(named) => named.path().map(|path| {
if path
.extension()
.is_some_and(|ext| ext == OsStr::new("svg"))
{
IcedHandle::Svg(svg::Handle::from_path(path))
} else {
IcedHandle::Image(image::Handle::from_path(path))
}
}),
Data::Image(handle) => Some(IcedHandle::Image(handle)),
Data::Svg(handle) => Some(IcedHandle::Svg(handle)),
};
match iced_handle {
Some(IcedHandle::Svg(handle)) => svg::Renderer::draw_svg(
renderer,
svg::Svg::new(handle),
icon_bounds,
),
Some(IcedHandle::Image(handle)) => {
image::Renderer::draw_image(
renderer,
(&handle).into(),
icon_bounds,
);
}
None => {}
}
}

View file

@ -0,0 +1,165 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
use super::{Handle, Icon};
use std::{borrow::Cow, path::PathBuf, sync::Arc};
#[derive(Debug, Clone, Default, Hash)]
/// Fallback icon to use if the icon was not found.
pub enum IconFallback {
#[default]
/// Default fallback using the icon name.
Default,
/// Fallback to specific icon names.
Names(Vec<Cow<'static, str>>),
}
#[must_use]
#[derive(derive_setters::Setters, Clone, Debug, Hash)]
pub struct Named {
/// Name of icon to locate in an XDG icon path.
pub(super) name: Arc<str>,
/// Checks for a fallback if the icon was not found.
pub fallback: Option<IconFallback>,
/// Restrict the lookup to a given scale.
#[setters(strip_option)]
pub scale: Option<u16>,
/// Restrict the lookup to a given size.
#[setters(strip_option)]
pub size: Option<u16>,
/// Whether the icon is symbolic or not.
pub symbolic: bool,
/// Prioritizes SVG over PNG
pub prefer_svg: bool,
}
impl Named {
pub fn new(name: impl Into<Arc<str>>) -> Self {
let name = name.into();
Self {
symbolic: name.ends_with("-symbolic"),
name,
fallback: Some(IconFallback::Default),
size: None,
scale: None,
prefer_svg: false,
}
}
#[cfg(not(windows))]
#[must_use]
pub fn path(self) -> Option<PathBuf> {
let name = &*self.name;
let fallback = &self.fallback;
let locate = |theme: &str, name| {
let mut lookup = freedesktop_icons::lookup(name)
.with_theme(theme.as_ref())
.with_cache();
if let Some(scale) = self.scale {
lookup = lookup.with_scale(scale);
}
if let Some(size) = self.size {
lookup = lookup.with_size(size);
}
if self.prefer_svg {
lookup = lookup.force_svg();
}
lookup.find()
};
let theme = "Papirus-Dark";
let themes = if theme.as_ref() == "Cosmic" {
vec![theme.as_ref()]
} else {
vec![theme.as_ref(), "Cosmic"]
};
let mut result = themes.iter().find_map(|t| locate(t, name));
// On failure, attempt to locate fallback icon.
if result.is_none() {
if matches!(fallback, Some(IconFallback::Default)) {
for new_name in name
.rmatch_indices('-')
.map(|(pos, _)| &name[..pos])
{
result = themes
.iter()
.find_map(|t| locate(t, new_name));
if result.is_some() {
break;
}
}
} else if let Some(IconFallback::Names(fallbacks)) =
fallback
{
for fallback in fallbacks {
result = themes
.iter()
.find_map(|t| locate(t, fallback));
if result.is_some() {
break;
}
}
}
}
result
}
#[cfg(windows)]
#[must_use]
pub fn path(self) -> Option<PathBuf> {
//TODO: implement icon lookup for Windows
None
}
#[inline]
pub fn handle(self) -> Handle {
Handle {
symbolic: self.symbolic,
data: super::Data::Name(self),
}
}
#[inline]
pub fn icon(self) -> Icon {
let size = self.size;
let icon = super::icon(self.handle());
match size {
Some(size) => icon.size(size),
None => icon,
}
}
}
impl From<Named> for Handle {
#[inline]
fn from(builder: Named) -> Self {
builder.handle()
}
}
impl From<Named> for Icon {
#[inline]
fn from(builder: Named) -> Self {
builder.icon()
}
}
impl<Message: 'static> From<Named> for crate::Element<'_, Message> {
#[inline]
fn from(builder: Named) -> Self {
builder.icon().into()
}
}

View file

@ -1 +1,2 @@
// pub mod slide_text; // pub mod 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 = cosmic::iced::wgpu::util::initialize_adapter_from_env_or_default(&instance, Some(&surface)) let adapter = iced::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 140)) (text "This is frodo" :font-size 90))
(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" :font-size 140 :font "Quicksand Bold" :font-size 60
: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" :font-size 140 :font "Quicksand Bold" :font-size 60
: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,11 +17,6 @@ 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