diff --git a/src/core/slide.rs b/src/core/slide.rs index a864f3d..e75c28e 100644 --- a/src/core/slide.rs +++ b/src/core/slide.rs @@ -1,6 +1,4 @@ -use cosmic::{ - cosmic_theme::palette::rgb::Rgba, widget::image::Handle, -}; +use cosmic::widget::image::Handle; // use cosmic::dialog::ashpd::url::Url; use crisp::types::{Keyword, Symbol, Value}; use iced_video_player::Video; @@ -12,7 +10,7 @@ use std::{ }; use tracing::error; -use crate::ui::text_svg::{Shadow, Stroke, TextSvg}; +use crate::ui::text_svg::{Color, Font, Shadow, Stroke, TextSvg}; use super::songs::Song; @@ -23,11 +21,12 @@ pub struct Slide { id: i32, pub(crate) background: Background, text: String, - font: String, + font: Option, font_size: i32, stroke: Option, shadow: Option, text_alignment: TextAlignment, + text_color: Option, audio: Option, video_loop: bool, video_start_time: f32, @@ -280,7 +279,7 @@ impl Slide { } pub fn set_font(mut self, font: impl AsRef) -> Self { - self.font = font.as_ref().into(); + self.font = Some(font.as_ref().into()); self } @@ -315,7 +314,7 @@ impl Slide { self.font_size } - pub fn font(&self) -> String { + pub fn font(&self) -> Option { self.font.clone() } @@ -331,6 +330,18 @@ impl Slide { self.pdf_page.clone() } + pub fn text_color(&self) -> Option { + self.text_color.clone() + } + + pub fn stroke(&self) -> Option { + self.stroke.clone() + } + + pub fn shadow(&self) -> Option { + self.shadow.clone() + } + pub const fn pdf_index(&self) -> u32 { self.pdf_index } @@ -543,9 +554,12 @@ pub fn lisp_to_background(lisp: &Value) -> Background { pub struct SlideBuilder { background: Option, text: Option, - font: Option, + font: Option, font_size: Option, audio: Option, + stroke: Option, + shadow: Option, + text_color: Option, text_alignment: Option, video_loop: Option, video_start_time: Option, @@ -584,12 +598,20 @@ impl SlideBuilder { self } + pub(crate) fn text_color( + mut self, + text_color: impl Into, + ) -> Self { + let _ = self.text_color.insert(text_color.into()); + self + } + pub(crate) fn audio(mut self, audio: impl Into) -> Self { let _ = self.audio.insert(audio.into()); self } - pub(crate) fn font(mut self, font: impl Into) -> Self { + pub(crate) fn font(mut self, font: impl Into) -> Self { let _ = self.font.insert(font.into()); self } @@ -599,6 +621,27 @@ impl SlideBuilder { self } + pub(crate) fn color(mut self, color: impl Into) -> Self { + let _ = self.text_color.insert(color.into()); + self + } + + pub(crate) fn stroke( + mut self, + stroke: impl Into, + ) -> Self { + let _ = self.stroke.insert(stroke.into()); + self + } + + pub(crate) fn shadow( + mut self, + shadow: impl Into, + ) -> Self { + let _ = self.shadow.insert(shadow.into()); + self + } + pub(crate) fn text_alignment( mut self, text_alignment: TextAlignment, @@ -656,9 +699,6 @@ impl SlideBuilder { let Some(text) = self.text else { return Err(miette!("No text")); }; - let Some(font) = self.font else { - return Err(miette!("No font")); - }; let Some(font_size) = self.font_size else { return Err(miette!("No font_size")); }; @@ -677,10 +717,13 @@ impl SlideBuilder { Ok(Slide { background, text, - font, + font: self.font, font_size, text_alignment, audio: self.audio, + stroke: self.stroke, + shadow: self.shadow, + text_color: self.text_color, video_loop, video_start_time, video_end_time, @@ -710,7 +753,7 @@ mod test { text: "This is frodo".to_string(), background: Background::try_from("~/pics/frodo.jpg") .unwrap(), - font: "Quicksand".to_string(), + font: Some("Quicksand".to_string().into()), font_size: 140, ..Default::default() } @@ -723,7 +766,7 @@ mod test { "~/vids/test/camprules2024.mp4", ) .unwrap(), - font: "Quicksand".to_string(), + font: Some("Quicksand".to_string().into()), ..Default::default() } } diff --git a/src/core/songs.rs b/src/core/songs.rs index 61cad43..bceb96c 100644 --- a/src/core/songs.rs +++ b/src/core/songs.rs @@ -3,7 +3,7 @@ use std::{ }; use cosmic::{ - cosmic_theme::palette::rgb::Rgba, + cosmic_theme::palette::{IntoColor, Srgb, rgb::Rgba}, iced::clipboard::mime::AsMimeTypes, }; use crisp::types::{Keyword, Symbol, Value}; @@ -16,7 +16,11 @@ use sqlx::{ }; use tracing::{debug, error}; -use crate::{Slide, SlideBuilder, core::slide}; +use crate::{ + Slide, SlideBuilder, + core::slide, + ui::text_svg::{self, Color, Font, Stroke, shadow, stroke}, +}; use super::{ content::Content, @@ -41,11 +45,12 @@ pub struct Song { pub text_alignment: Option, pub font: Option, pub font_size: Option, - pub stroke_size: Option, - pub stroke_color: Option, - pub shadow_size: Option, - pub shadow_offset: Option<(i32, i32)>, - pub shadow_color: Option, + pub text_color: Option, + pub stroke_size: Option, + pub stroke_color: Option, + pub shadow_size: Option, + pub shadow_offset: Option<(i16, i16)>, + pub shadow_color: Option, pub verses: Option>, pub verse_map: Option>, } @@ -283,15 +288,58 @@ impl ServiceTrait for Song { let slides: Vec = lyrics .iter() .filter_map(|l| { - SlideBuilder::new() + let font = + Font::default() + .name( + self.font + .clone() + .unwrap_or_else(|| "Calibri".into()), + ) + .size(self.font_size.unwrap_or_else(|| 100) + as u8); + let stroke_size = + self.stroke_size.unwrap_or_default(); + let stroke: Stroke = stroke( + stroke_size, + self.stroke_color + .map(|color| Color::from(color)) + .unwrap_or_default(), + ); + let shadow_size = + self.shadow_size.unwrap_or_default(); + let shadow = shadow( + self.shadow_offset.unwrap_or_default().0, + self.shadow_offset.unwrap_or_default().1, + shadow_size, + self.shadow_color + .map(|color| Color::from(color)) + .unwrap_or_default(), + ); + let builder = SlideBuilder::new(); + let builder = if shadow_size > 0 { + builder.shadow(shadow) + } else { + builder + }; + let builder = if stroke_size > 0 { + builder.stroke(stroke) + } else { + builder + }; + builder .background( self.background.clone().unwrap_or_default(), ) - .font(self.font.clone().unwrap_or_default()) + .font(font) .font_size(self.font_size.unwrap_or_default()) .text_alignment( self.text_alignment.unwrap_or_default(), ) + .text_color( + self.text_color.unwrap_or_else(|| { + Srgb::new(1.0, 1.0, 1.0) + }), + ) .audio(self.audio.clone().unwrap_or_default()) .video_loop(true) .video_start_time(0.0) diff --git a/src/ui/presenter.rs b/src/ui/presenter.rs index f990015..ebfa664 100644 --- a/src/ui/presenter.rs +++ b/src/ui/presenter.rs @@ -458,8 +458,12 @@ impl Presenter { // self.current_slide_index = slide; debug!("cloning slide..."); self.current_slide = slide.clone(); - let _ = - self.update(Message::ChangeFont(slide.font())); + let font = if let Some(font) = slide.font() { + font.get_name() + } else { + "".into() + }; + let _ = self.update(Message::ChangeFont(font)); debug!("changing video now..."); if !backgrounds_match { if let Some(video) = &mut self.video { diff --git a/src/ui/song_editor.rs b/src/ui/song_editor.rs index cf6174e..c4c9350 100644 --- a/src/ui/song_editor.rs +++ b/src/ui/song_editor.rs @@ -81,7 +81,7 @@ pub struct SongEditor { song_slides: Option>, slide_state: SlideEditor, stroke_sizes: combo_box::State, - stroke_size: i32, + stroke_size: u16, stroke_open: bool, #[debug(skip)] stroke_color_model: ColorPickerModel, @@ -117,7 +117,7 @@ pub enum Message { None, ChangeAuthor(String), PauseVideo, - UpdateStrokeSize(i32), + UpdateStrokeSize(u16), UpdateStrokeColor(ColorPickerUpdate), OpenStroke, CloseStroke, @@ -310,7 +310,7 @@ impl SongEditor { if let Some(song) = &mut self.song { song.font = Some(font); let song = song.to_owned(); - return self.update_song(song); + return Action::Task(self.update_song(song)); } } Message::ChangeFontSize(size) => { @@ -318,7 +318,7 @@ impl SongEditor { if let Some(song) = &mut self.song { song.font_size = Some(size as i32); let song = song.to_owned(); - return self.update_song(song); + return Action::Task(self.update_song(song)); } } Message::ChangeTitle(title) => { @@ -326,7 +326,7 @@ impl SongEditor { if let Some(song) = &mut self.song { song.title = title; let song = song.to_owned(); - return self.update_song(song); + return Action::Task(self.update_song(song)); } } Message::ChangeVerseOrder(verse_order) => { @@ -337,7 +337,7 @@ impl SongEditor { .map(std::borrow::ToOwned::to_owned) .collect(); song.verse_order = Some(verse_order); - return self.update_song(song); + return Action::Task(self.update_song(song)); } } Message::ChangeLyrics(action) => { @@ -347,7 +347,7 @@ impl SongEditor { if let Some(mut song) = self.song.clone() { song.lyrics = Some(lyrics); - return self.update_song(song); + return Action::Task(self.update_song(song)); } } Message::Edit(edit) => { @@ -359,7 +359,7 @@ impl SongEditor { self.author = author.clone(); if let Some(mut song) = self.song.clone() { song.author = Some(author); - return self.update_song(song); + return Action::Task(self.update_song(song)); } } Message::ChangeBackground(Ok(path)) => { @@ -368,7 +368,7 @@ impl SongEditor { let background = Background::try_from(path).ok(); self.background_video(&background); song.background = background; - return self.update_song(song); + return Action::Task(self.update_song(song)); } } Message::ChangeBackground(Err(error)) => { @@ -394,19 +394,21 @@ impl SongEditor { song.stroke_size = Some(size); } let song = song.to_owned(); - return self.update_song(song); + return Action::Task(self.update_song(song)); } } Message::UpdateStrokeColor(update) => { - let task = self.stroke_color_model.update(update); - if let Some(song) = self.song.as_mut() + let mut tasks = Vec::with_capacity(2); + tasks.push(self.stroke_color_model.update(update)); + if let Some(mut song) = self.song.clone() && let Some(color) = self.stroke_color_model.get_applied_color() { debug!(?color); song.stroke_color = Some(color.into()); + tasks.push(self.update_song(song)); } - return Action::Task(task); + return Action::Task(Task::batch(tasks)); } Message::UpdateSlides(slides) => { self.song_slides = Some(slides); @@ -473,7 +475,9 @@ impl SongEditor { &old_verse_name, ); - return self.update_song(song); + return Action::Task( + self.update_song(song), + ); } } verse_editor::Action::UpdateVerse(( @@ -486,14 +490,18 @@ impl SongEditor { // song.update_verse( // index, verse, lyric, // ); - return self.update_song(song); + return Action::Task( + self.update_song(song), + ); } } verse_editor::Action::DeleteVerse(verse) => { if let Some(mut song) = self.song.clone() { song.delete_verse(verse); - return self.update_song(song); + return Action::Task( + self.update_song(song), + ); }; } verse_editor::Action::None => (), @@ -517,7 +525,7 @@ impl SongEditor { } if let Some(mut song) = self.song.clone() { song.add_verse(verse, lyric); - return self.update_song(song); + return Action::Task(self.update_song(song)); } } Message::RemoveVerse(index) => { @@ -528,7 +536,7 @@ impl SongEditor { verses.remove(index); }, ); - return self.update_song(song); + return Action::Task(self.update_song(song)); } } Message::ChipHovered(index) => { @@ -546,7 +554,9 @@ impl SongEditor { { verses.insert(index, verse); let song = song.clone(); - return self.update_song(song); + return Action::Task( + self.update_song(song), + ); } error!("No verses in this song?"); } else { @@ -567,7 +577,9 @@ impl SongEditor { { verses.push(verse); let song = song.clone(); - return self.update_song(song); + return Action::Task( + self.update_song(song), + ); } else { error!( "No verses in this song or no song here" @@ -592,7 +604,7 @@ impl SongEditor { let verse = verses.remove(index); verses.insert(target_index, verse); debug!(?verses); - return self.update_song(song); + return Action::Task(self.update_song(song)); } } draggable::DragEvent::Canceled { index } => (), @@ -606,7 +618,7 @@ impl SongEditor { Message::SetTextAlignment(alignment) => { if let Some(mut song) = self.song.clone() { song.text_alignment = Some(alignment); - return self.update_song(song); + return Action::Task(self.update_song(song)); } } Message::None => (), @@ -1147,7 +1159,7 @@ impl SongEditor { dropdown( &["0", "1", "2", "3", "4", "5", "6", "7"], Some(self.stroke_size as usize), - |i| Message::UpdateStrokeSize(i as i32), + |i| Message::UpdateStrokeSize(i as u16), ) .gap(5.0), ] @@ -1389,11 +1401,11 @@ impl SongEditor { self.editing } - fn update_song(&mut self, song: Song) -> Action { - use cosmic::iced_futures::futures::stream; + fn update_song(&mut self, song: Song) -> Task { + // use cosmic::iced_futures::futures::stream; // use cosmic::iced_futures::futures::{Stream, StreamExt}; // use cosmic::iced_futures::stream::channel; - use cosmic::task::stream; + // use cosmic::task::stream; let font_db = Arc::clone(&self.font_db); // need to test to see which of these methods yields faster // text_svg slide creation. There is a small thought in me that @@ -1464,7 +1476,7 @@ impl SongEditor { } tasks.push(Task::done(Message::UpdateSong(song))); - Action::Task(Task::batch(tasks)) + Task::batch(tasks) } fn background_video(&mut self, background: &Option) { diff --git a/src/ui/text_svg.rs b/src/ui/text_svg.rs index f5526ec..f798acd 100644 --- a/src/ui/text_svg.rs +++ b/src/ui/text_svg.rs @@ -21,7 +21,7 @@ use resvg::{ usvg::{Tree, fontdb}, }; use serde::{Deserialize, Serialize}; -use tracing::error; +use tracing::{debug, error}; use crate::TextAlignment; @@ -378,7 +378,11 @@ impl TextSvg { stroke.color, stroke.size )); } - final_svg.push_str(" style=\"filter:url(#shadow);\">"); + + if self.shadow.is_some() { + final_svg.push_str(" style=\"filter:url(#shadow);\""); + } + final_svg.push_str(">"); let text: String = self .text @@ -496,20 +500,7 @@ pub fn text_svg_generator( slide: &mut crate::core::slide::Slide, fontdb: Arc, ) { - if !slide.text().is_empty() { - 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()) - .size(slide.font_size().try_into().unwrap()), - ) - .fontdb(Arc::clone(&fontdb)) - .build(Size::new(1280.0, 720.0), true); - slide.text_svg = Some(text_svg); - } + text_svg_generator_with_cache(slide, fontdb, true); } pub fn text_svg_generator_with_cache( @@ -518,17 +509,31 @@ pub fn text_svg_generator_with_cache( cache: bool, ) { if !slide.text().is_empty() { + let font = if let Some(font) = slide.font() { + font + } else { + Font::default() + }; 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()) - .size(slide.font_size().try_into().unwrap()), - ) - .fontdb(Arc::clone(&fontdb)) - .build(Size::new(1280.0, 720.0), cache); + .fill( + slide.text_color().unwrap_or_else(|| "#fff".into()), + ); + let text_svg = if let Some(stroke) = slide.stroke() { + text_svg.stroke(stroke) + } else { + text_svg + }; + let text_svg = if let Some(shadow) = slide.shadow() { + text_svg.shadow(shadow) + } else { + text_svg + }; + let text_svg = + text_svg.font(font).fontdb(Arc::clone(&fontdb)); + debug!(fill = ?text_svg.fill, font = ?text_svg.font, stroke = ?text_svg.stroke, shadow = ?text_svg.shadow, text = ?text_svg.text); + let text_svg = + text_svg.build(Size::new(1280.0, 720.0), cache); slide.text_svg = Some(text_svg); } } @@ -538,6 +543,7 @@ mod tests { use crate::core::slide::Slide; use super::*; + use rayon::iter::{IntoParallelIterator, ParallelIterator}; use resvg::usvg::fontdb::Database; #[test] @@ -547,12 +553,12 @@ mod tests { let mut fontdb = Database::new(); fontdb.load_system_fonts(); let fontdb = Arc::new(fontdb); - (0..100).for_each(|index| { + (0..40).into_par_iter().for_each(|_| { let mut slide = slide .clone() .set_font_size(120) .set_font("Quicksand") - .set_text(index.to_string()); + .set_text("This is the first slide of text\nAnd we are singing\nTo save the world!"); text_svg_generator_with_cache( &mut slide, Arc::clone(&fontdb),