update slides and songs to generate strokes and shadows in text_svg
Some checks are pending
/ test (push) Waiting to run

This commit is contained in:
Chris Cochrun 2026-02-11 06:25:55 -06:00
parent 3b960c5a17
commit 4bd8cf04d4
5 changed files with 193 additions and 80 deletions

View file

@ -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>,
font_size: i32,
stroke: Option<Stroke>,
shadow: Option<Shadow>,
text_alignment: TextAlignment,
text_color: Option<Color>,
audio: Option<PathBuf>,
video_loop: bool,
video_start_time: f32,
@ -280,7 +279,7 @@ impl Slide {
}
pub fn set_font(mut self, font: impl AsRef<str>) -> 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<Font> {
self.font.clone()
}
@ -331,6 +330,18 @@ impl Slide {
self.pdf_page.clone()
}
pub fn text_color(&self) -> Option<Color> {
self.text_color.clone()
}
pub fn stroke(&self) -> Option<Stroke> {
self.stroke.clone()
}
pub fn shadow(&self) -> Option<Shadow> {
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<Background>,
text: Option<String>,
font: Option<String>,
font: Option<Font>,
font_size: Option<i32>,
audio: Option<PathBuf>,
stroke: Option<Stroke>,
shadow: Option<Shadow>,
text_color: Option<Color>,
text_alignment: Option<TextAlignment>,
video_loop: Option<bool>,
video_start_time: Option<f32>,
@ -584,12 +598,20 @@ impl SlideBuilder {
self
}
pub(crate) fn text_color(
mut self,
text_color: impl Into<Color>,
) -> Self {
let _ = self.text_color.insert(text_color.into());
self
}
pub(crate) fn audio(mut self, audio: impl Into<PathBuf>) -> Self {
let _ = self.audio.insert(audio.into());
self
}
pub(crate) fn font(mut self, font: impl Into<String>) -> Self {
pub(crate) fn font(mut self, font: impl Into<Font>) -> Self {
let _ = self.font.insert(font.into());
self
}
@ -599,6 +621,27 @@ impl SlideBuilder {
self
}
pub(crate) fn color(mut self, color: impl Into<Color>) -> Self {
let _ = self.text_color.insert(color.into());
self
}
pub(crate) fn stroke(
mut self,
stroke: impl Into<Stroke>,
) -> Self {
let _ = self.stroke.insert(stroke.into());
self
}
pub(crate) fn shadow(
mut self,
shadow: impl Into<Shadow>,
) -> 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()
}
}

View file

@ -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<TextAlignment>,
pub font: Option<String>,
pub font_size: Option<i32>,
pub stroke_size: Option<i32>,
pub stroke_color: Option<Rgba>,
pub shadow_size: Option<i32>,
pub shadow_offset: Option<(i32, i32)>,
pub shadow_color: Option<Rgba>,
pub text_color: Option<Srgb>,
pub stroke_size: Option<u16>,
pub stroke_color: Option<Srgb>,
pub shadow_size: Option<u16>,
pub shadow_offset: Option<(i16, i16)>,
pub shadow_color: Option<Srgb>,
pub verses: Option<Vec<VerseName>>,
pub verse_map: Option<HashMap<VerseName, String>>,
}
@ -283,15 +288,58 @@ impl ServiceTrait for Song {
let slides: Vec<Slide> = 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)

View file

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

View file

@ -81,7 +81,7 @@ pub struct SongEditor {
song_slides: Option<Vec<Slide>>,
slide_state: SlideEditor,
stroke_sizes: combo_box::State<i32>,
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<Message> {
// 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<Background>) {

View file

@ -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<fontdb::Database>,
) {
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),