Compare commits

..

11 commits

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

2352
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -8,7 +8,6 @@ description = "A cli presentation system"
[dependencies]
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"
miette = { version = "7.2.0", features = ["fancy"] }
pretty_assertions = "1.4.1"
@ -33,23 +32,22 @@ gstreamer-app = "0.23"
url = "2"
colors-transform = "0.2.11"
rayon = "1.11.0"
# resvg = "0.45.1"
resvg = "0.45.1"
image = "0.25.8"
# femtovg = { version = "0.16.0", features = ["wgpu"] }
# wgpu = "26.0.1"
# mupdf = "0.5.0"
rfd = { version = "0.12.1", features = ["xdg-portal"], default-features = false }
derive_setters = "0.1.8"
freedesktop-icons = "0.4.0"
# rfd = { version = "0.12.1", features = ["xdg-portal"], default-features = false }
[dependencies.iced]
git = "https://github.com/iced-rs/iced"
branch = "master"
features = ["wgpu", "image", "advanced", "svg", "canvas", "hot", "debug", "lazy", "tokio"]
[dependencies.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"]
[dependencies.iced_video_player]
git = "https://git.tfcconnection.org/chris/iced_video_player"
branch = "master"
# branch = "cosmic"
git = "https://github.com/jackpot51/iced_video_player.git"
branch = "cosmic"
features = ["wgpu"]
# [profile.dev]
# opt-level = 3

View file

@ -1,5 +1,6 @@
use std::mem::replace;
use cosmic::iced::Executor;
use miette::{miette, Result};
use sqlx::{Connection, SqliteConnection};

View file

@ -1,11 +1,12 @@
use std::borrow::Cow;
use std::cmp::Ordering;
use std::ops::Deref;
use std::sync::Arc;
use std::sync::{Arc, Mutex};
use cosmic::iced::clipboard::mime::{AllowedMimeTypes, AsMimeTypes};
use crisp::types::{Keyword, Symbol, Value};
// use cosmic::iced::clipboard::mime::{AllowedMimeTypes, AsMimeTypes};
use miette::Result;
use resvg::usvg::fontdb;
use tracing::{debug, error};
use crate::Slide;
@ -17,13 +18,13 @@ use super::videos::Video;
use super::kinds::ServiceItemKind;
#[derive(Debug, PartialEq, Clone)]
#[derive(Debug, Clone, PartialEq)]
pub struct ServiceItem {
pub id: i32,
pub title: String,
pub database_id: i32,
pub kind: ServiceItemKind,
pub slides: Arc<[Slide]>,
pub slides: Vec<Slide>,
// pub item: Box<dyn ServiceTrait>,
}
@ -56,29 +57,29 @@ impl TryFrom<(Vec<u8>, String)> for ServiceItem {
}
}
// impl AllowedMimeTypes for ServiceItem {
// fn allowed() -> Cow<'static, [String]> {
// Cow::from(vec!["application/service-item".to_string()])
// }
// }
impl AllowedMimeTypes for ServiceItem {
fn allowed() -> Cow<'static, [String]> {
Cow::from(vec!["application/service-item".to_string()])
}
}
// impl AsMimeTypes for ServiceItem {
// fn available(&self) -> Cow<'static, [String]> {
// debug!(?self);
// Cow::from(vec!["application/service-item".to_string()])
// }
impl AsMimeTypes for ServiceItem {
fn available(&self) -> Cow<'static, [String]> {
debug!(?self);
Cow::from(vec!["application/service-item".to_string()])
}
// fn as_bytes(
// &self,
// mime_type: &str,
// ) -> Option<std::borrow::Cow<'static, [u8]>> {
// debug!(?self);
// debug!(mime_type);
// let val = Value::from(self);
// let val = String::from(val);
// Some(Cow::from(val.into_bytes()))
// }
// }
fn as_bytes(
&self,
mime_type: &str,
) -> Option<std::borrow::Cow<'static, [u8]>> {
debug!(?self);
debug!(mime_type);
let val = Value::from(self);
let val = String::from(val);
Some(Cow::from(val.into_bytes()))
}
}
impl From<&ServiceItem> for Value {
fn from(value: &ServiceItem) -> Self {
@ -121,7 +122,7 @@ impl Default for ServiceItem {
title: String::default(),
database_id: 0,
kind: ServiceItemKind::Content(Slide::default()),
slides: Arc::new([]),
slides: vec![],
// item: Box::new(Image::default()),
}
}
@ -171,7 +172,7 @@ impl From<&Value> for ServiceItem {
kind: ServiceItemKind::Content(
slide.clone(),
),
slides: Arc::new([slide]),
slides: vec![slide],
}
} else if let Some(background) =
list.get(background_pos)

View file

@ -1,11 +1,13 @@
// use iced::dialog::ashpd::url::Url;
// use cosmic::dialog::ashpd::url::Url;
use crisp::types::{Keyword, Symbol, Value};
use iced_video_player::Video;
use miette::{miette, Result};
use resvg::usvg::fontdb;
use serde::{Deserialize, Serialize};
use std::{
fmt::Display,
path::{Path, PathBuf},
sync::Arc,
};
use tracing::error;
@ -13,6 +15,40 @@ use crate::ui::text_svg::{self, TextSvg};
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(
Clone,
Copy,
@ -203,15 +239,6 @@ impl Display for ParseError {
}
}
#[derive(
Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize,
)]
pub enum BackgroundKind {
#[default]
Image,
Video,
}
impl From<String> for BackgroundKind {
fn from(value: String) -> Self {
if value == "image" {
@ -222,24 +249,6 @@ impl From<String> for BackgroundKind {
}
}
#[derive(
Clone, Debug, Default, PartialEq, Serialize, Deserialize,
)]
pub struct Slide {
id: i32,
background: Background,
text: String,
font: String,
font_size: i32,
text_alignment: TextAlignment,
audio: Option<PathBuf>,
video_loop: bool,
video_start_time: f32,
video_end_time: f32,
#[serde(skip)]
pub text_svg: TextSvg,
}
impl From<&Slide> for Value {
fn from(value: &Slide) -> Self {
Self::List(vec![Self::Symbol(Symbol("slide".into()))])
@ -252,6 +261,11 @@ impl Slide {
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 {
self.font = font.as_ref().into();
self
@ -275,6 +289,10 @@ impl Slide {
self.text.clone()
}
pub fn text_alignment(&self) -> TextAlignment {
self.text_alignment.clone()
}
pub fn font_size(&self) -> i32 {
self.font_size
}
@ -614,55 +632,22 @@ impl SlideBuilder {
let Some(video_end_time) = self.video_end_time else {
return Err(miette!("No video_end_time"));
};
if let Some(text_svg) = self.text_svg {
Ok(Slide {
background,
text,
font,
font_size,
text_alignment,
audio: self.audio,
video_loop,
video_start_time,
video_end_time,
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()
})
}
Ok(Slide {
background,
text,
font,
font_size,
text_alignment,
audio: self.audio,
video_loop,
video_start_time,
video_end_time,
text_svg: self.text_svg,
..Default::default()
})
}
}
#[derive(Debug, Clone, Default)]
struct Image {
pub source: String,
pub fit: String,
pub children: Vec<String>,
}
impl Image {
fn new() -> Self {
Self {

View file

@ -1,5 +1,6 @@
use std::{collections::HashMap, option::Option, path::PathBuf};
use cosmic::iced::Executor;
use crisp::types::{Keyword, Symbol, Value};
use miette::{miette, IntoDiagnostic, Result};
use serde::{Deserialize, Serialize};

View file

@ -7,6 +7,7 @@ use super::{
service_items::ServiceTrait,
slide::Slide,
};
use cosmic::iced::Executor;
use crisp::types::{Keyword, Symbol, Value};
use miette::{IntoDiagnostic, Result};
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 iced::Length;
use cosmic::iced::Length;
struct DoubleSlider<'a, T, Message> {
range: RangeInclusive<T>,

View file

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

View file

@ -1,29 +1,36 @@
use miette::{IntoDiagnostic, Result};
use std::{fs::File, io::BufReader, path::PathBuf, sync::Arc};
use iced::{
alignment::Horizontal,
border,
font::{Family, Stretch, Style, Weight},
widget::{
container, image, mouse_area, responsive, rich_text,
scrollable::{
self, scroll_to, AbsoluteOffset, Direction, Id, Scrollbar,
},
span, stack, text, vertical_rule, Column, Container, Row,
Space,
use cosmic::{
iced::{
alignment::Horizontal,
border,
font::{Family, Stretch, Style, Weight},
Background, Border, Color, ContentFit, Font, Length, Shadow,
Vector,
},
Background, Border, Color, ContentFit, Element, Font, Length,
Shadow, Task, Vector,
iced_widget::{
rich_text,
scrollable::{
scroll_to, AbsoluteOffset, Direction, Scrollbar,
},
span, stack, vertical_rule,
},
prelude::*,
widget::{
container, image, mouse_area, responsive, scrollable, text,
Column, Container, Id, Image, Row, Space,
},
Task,
};
use iced_video_player::{Position, Video, VideoPlayer};
use iced_video_player::{gst_pbutils, Position, Video, VideoPlayer};
use rodio::{Decoder, OutputStream, Sink};
use tracing::{debug, error, info, warn};
use url::Url;
use crate::{
core::{service_items::ServiceItem, slide::Slide},
// ui::widgets::slide_text,
ui::text_svg,
BackgroundKind,
};
@ -125,7 +132,9 @@ impl Presenter {
Some(v)
}
Err(e) => {
error!("Had an error creating the video object: {e}, likely the first slide isn't a video");
error!(
"Had an error creating the video object: {e}, likely the first slide isn't a video"
);
None
}
}
@ -141,6 +150,7 @@ impl Presenter {
};
let total_slides: usize =
items.iter().fold(0, |a, item| a + item.slides.len());
Self {
current_slide: items[0].slides[0].clone(),
current_item: 0,
@ -161,7 +171,7 @@ impl Presenter {
)
},
scroll_id: Id::unique(),
current_font: iced::font::Font::DEFAULT,
current_font: cosmic::font::default(),
}
}
@ -337,27 +347,27 @@ impl Presenter {
return Action::Task(Task::perform(
async move {
tokio::task::spawn_blocking(move || {
// match gst_pbutils::MissingPluginMessage::parse(&element) {
// Ok(missing_plugin) => {
// let mut install_ctx = gst_pbutils::InstallPluginsContext::new();
// install_ctx
// .set_desktop_id(&format!("{}.desktop", "org.chriscochrun.lumina"));
// let install_detail = missing_plugin.installer_detail();
// println!("installing plugins: {}", install_detail);
// let status = gst_pbutils::missing_plugins::install_plugins_sync(
// &[&install_detail],
// Some(&install_ctx),
// );
// info!("plugin install status: {}", status);
// info!(
// "gstreamer registry update: {:?}",
// gstreamer::Registry::update()
// );
// }
// Err(err) => {
// warn!("failed to parse missing plugin message: {err}");
// }
// }
match gst_pbutils::MissingPluginMessage::parse(&element) {
Ok(missing_plugin) => {
let mut install_ctx = gst_pbutils::InstallPluginsContext::new();
install_ctx
.set_desktop_id(&format!("{}.desktop", "org.chriscochrun.lumina"));
let install_detail = missing_plugin.installer_detail();
println!("installing plugins: {}", install_detail);
let status = gst_pbutils::missing_plugins::install_plugins_sync(
&[&install_detail],
Some(&install_ctx),
);
info!("plugin install status: {}", status);
info!(
"gstreamer registry update: {:?}",
gstreamer::Registry::update()
);
}
Err(err) => {
warn!("failed to parse missing plugin message: {err}");
}
}
Message::None
})
.await
@ -370,7 +380,7 @@ impl Presenter {
self.hovered_slide = slide;
}
Message::StartAudio => {
return Action::Task(self.start_audio())
return Action::Task(self.start_audio());
}
Message::EndAudio => {
self.sink.1.stop();
@ -442,7 +452,7 @@ impl Presenter {
.style(move |t| {
let mut style =
container::Style::default();
let theme = t;
let theme = t.cosmic();
let hovered = self.hovered_slide
== Some((
item_index,
@ -452,25 +462,19 @@ impl Presenter {
Some(Background::Color(
if is_current_slide {
theme
.extended_palette(
)
.secondary
.strong
.color
.accent
.base
.into()
} else if hovered {
theme
.extended_palette(
)
.secondary
.strong
.color
.accent
.hover
.into()
} else {
theme
.extended_palette(
)
.background
.neutral
.color
.palette
.neutral_3
.into()
},
));
style.border = Border::default()
@ -503,7 +507,7 @@ impl Presenter {
.padding(10),
)
.interaction(
iced::mouse::Interaction::Pointer,
cosmic::iced::mouse::Interaction::Pointer,
)
.on_move(move |_| {
Message::HoveredSlide(Some((
@ -521,11 +525,11 @@ impl Presenter {
let row = Row::from_vec(slides)
.spacing(10)
.padding([20, 15]);
let label = text(item.title.clone());
let label = text::body(item.title.clone());
let label_container = container(label)
.align_top(Length::Fill)
.align_left(Length::Fill)
.padding([0, 35]);
.padding([0, 0, 0, 35]);
let divider = vertical_rule(2);
items.push(
container(stack!(row, label_container))
@ -535,16 +539,15 @@ impl Presenter {
items.push(divider.into());
},
);
let row = scrollable::Scrollable::new(
container(Row::from_vec(items)).style(|t| {
let row =
scrollable(container(Row::from_vec(items)).style(|t| {
let style = container::Style::default();
style.border(Border::default().width(2))
}),
)
.direction(Direction::Horizontal(Scrollbar::new()))
.height(Length::Fill)
.width(Length::Fill)
.id(self.scroll_id.clone());
}))
.direction(Direction::Horizontal(Scrollbar::new()))
.height(Length::Fill)
.width(Length::Fill)
.id(self.scroll_id.clone());
row.into()
}
@ -578,7 +581,7 @@ impl Presenter {
// Container::new(container)
// .style(move |t| {
// let mut style = container::Style::default();
// let theme = t.iced();
// let theme = t.cosmic();
// let hovered = self.hovered_slide == slide_id;
// style.background = Some(Background::Color(
// if is_current_slide {
@ -617,7 +620,7 @@ impl Presenter {
// .height(100)
// .padding(10),
// )
// .interaction(iced::iced::mouse::Interaction::Pointer)
// .interaction(cosmic::iced::mouse::Interaction::Pointer)
// .on_move(move |_| Message::HoveredSlide(slide_id))
// .on_exit(Message::HoveredSlide(-1))
// .on_press(Message::SlideChange(slide.clone()));
@ -640,7 +643,9 @@ impl Presenter {
self.video = Some(v)
}
Err(e) => {
error!("Had an error creating the video object: {e}");
error!(
"Had an error creating the video object: {e}"
);
self.video = None;
}
}
@ -702,92 +707,100 @@ pub(crate) fn slide_view(
let slide_text = slide.text();
// let font = SvgFont::from(font).size(font_size.floor() as u8);
let text_container = if delegate {
// 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 =
// 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 text_container = if delegate {
// // 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)
// } 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)
// .center(Length::Fill)
@ -795,6 +808,21 @@ pub(crate) fn slide_view(
// let text_stack =
// 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))
.style(|_| {
container::background(Background::Color(Color::BLACK))
@ -802,7 +830,7 @@ pub(crate) fn slide_view(
.clip(true)
.width(width)
.height(size.height);
let container = match slide.background().kind {
let background = match slide.background().kind {
BackgroundKind::Image => {
let path = slide.background().path.clone();
Container::new(
@ -829,15 +857,15 @@ pub(crate) fn slide_view(
} else if let Some(video) = &video {
Container::new(
VideoPlayer::new(video)
// .mouse_hidden(hide_mouse)
.mouse_hidden(hide_mouse)
.width(width)
.height(size.height)
.on_end_of_stream(Message::EndVideo)
.on_new_frame(Message::VideoFrame)
// .on_missing_plugin(Message::MissingPlugin)
// .on_warning(|w| {
// Message::Error(w.to_string())
// })
.on_missing_plugin(Message::MissingPlugin)
.on_warning(|w| {
Message::Error(w.to_string())
})
.on_error(|e| {
Message::Error(e.to_string())
})
@ -851,11 +879,8 @@ pub(crate) fn slide_view(
}
}
};
let stack = stack!(
black,
container.center(Length::Fill),
text_container
);
let stack =
stack!(black, background.center(Length::Fill), text);
Container::new(stack).center(Length::Fill).into()
});
// let vid = if let Some(video) = &video {

View file

@ -1,12 +1,14 @@
use std::{io, path::PathBuf};
use iced::{
use cosmic::{
iced::{Color, Font, Length, Size},
prelude::*,
widget::{
self,
canvas::{self, Program, Stroke},
container, Canvas,
},
Color, Font, Length, Renderer, Size,
Renderer,
};
use tracing::debug;
@ -49,14 +51,14 @@ pub enum SlideError {
#[derive(Debug, Default)]
struct EditorProgram {
mouse_button_pressed: Option<iced::mouse::Button>,
mouse_button_pressed: Option<cosmic::iced::mouse::Button>,
}
impl SlideEditor {
pub fn view<'a>(
&'a self,
font: Font,
) -> iced::Element<'a, SlideWidget> {
) -> cosmic::Element<'a, SlideWidget> {
container(
widget::canvas(&self.program)
.height(Length::Fill)
@ -66,9 +68,9 @@ impl SlideEditor {
}
}
/// Ensure to use the `iced::Theme and iced::Renderer` here
/// Ensure to use the `cosmic::Theme and cosmic::Renderer` here
/// or else it will not compile
impl<'a> Program<SlideWidget, iced::Theme, iced::Renderer>
impl<'a> Program<SlideWidget, cosmic::Theme, cosmic::Renderer>
for EditorProgram
{
type State = ();
@ -77,9 +79,9 @@ impl<'a> Program<SlideWidget, iced::Theme, iced::Renderer>
&self,
state: &Self::State,
renderer: &Renderer,
theme: &iced::Theme,
bounds: iced::Rectangle,
cursor: iced::mouse::Cursor,
theme: &cosmic::Theme,
bounds: cosmic::iced::Rectangle,
cursor: cosmic::iced_core::mouse::Cursor,
) -> Vec<canvas::Geometry<Renderer>> {
// We prepare a new `Frame`
let mut frame = canvas::Frame::new(renderer, bounds.size());
@ -88,7 +90,7 @@ impl<'a> Program<SlideWidget, iced::Theme, iced::Renderer>
// We create a `Path` representing a simple circle
let circle = canvas::Path::circle(frame.center(), 50.0);
let border = canvas::Path::rectangle(
iced::Point { x: 10.0, y: 10.0 },
cosmic::iced::Point { x: 10.0, y: 10.0 },
Size::new(frame_rect.width, frame_rect.height),
);
@ -114,19 +116,21 @@ impl<'a> Program<SlideWidget, iced::Theme, iced::Renderer>
fn update(
&self,
_state: &mut Self::State,
event: &iced::Event,
bounds: iced::Rectangle,
_cursor: iced::mouse::Cursor,
) -> std::option::Option<iced::widget::Action<SlideWidget>> {
event: canvas::Event,
bounds: cosmic::iced::Rectangle,
_cursor: cosmic::iced_core::mouse::Cursor,
) -> (canvas::event::Status, Option<SlideWidget>) {
match event {
iced::Event::Mouse(event) => match event {
iced::mouse::Event::CursorEntered => {
canvas::Event::Mouse(event) => match event {
cosmic::iced::mouse::Event::CursorEntered => {
debug!("cursor entered")
}
iced::mouse::Event::CursorLeft => {
cosmic::iced::mouse::Event::CursorLeft => {
debug!("cursor left")
}
iced::mouse::Event::CursorMoved { position } => {
cosmic::iced::mouse::Event::CursorMoved {
position,
} => {
if bounds.x < position.x
&& bounds.y < position.y
&& (bounds.width + bounds.x) > position.x
@ -135,34 +139,29 @@ impl<'a> Program<SlideWidget, iced::Theme, iced::Renderer>
debug!(?position, "cursor moved");
}
}
iced::mouse::Event::ButtonPressed(button) => {
cosmic::iced::mouse::Event::ButtonPressed(button) => {
// self.mouse_button_pressed = Some(button);
debug!(?button, "mouse button pressed")
}
iced::mouse::Event::ButtonReleased(button) => {
debug!(?button, "mouse button released")
}
iced::mouse::Event::WheelScrolled { delta } => {
debug!(?delta, "scroll wheel")
}
cosmic::iced::mouse::Event::ButtonReleased(
button,
) => debug!(?button, "mouse button released"),
cosmic::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!(),
canvas::Event::Touch(event) => debug!("test"),
canvas::Event::Keyboard(event) => debug!("test"),
}
None
(canvas::event::Status::Ignored, None)
}
fn mouse_interaction(
&self,
_state: &Self::State,
_bounds: iced::Rectangle,
_cursor: iced::mouse::Cursor,
) -> iced::mouse::Interaction {
iced::mouse::Interaction::default()
_bounds: cosmic::iced::Rectangle,
_cursor: cosmic::iced_core::mouse::Cursor,
) -> cosmic::iced_core::mouse::Interaction {
cosmic::iced_core::mouse::Interaction::default()
}
}

View file

@ -1,25 +1,27 @@
use std::{io, path::PathBuf};
use std::{io, path::PathBuf, sync::Arc};
use dirs::font_dir;
use 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,
use cosmic::{
dialog::file_chooser::open::Dialog,
iced::{
font::{Family, Stretch, Style, Weight},
Font, Length,
},
Element, Font, Length, Task,
iced_wgpu::graphics::text::cosmic_text::fontdb,
iced_widget::row,
theme,
widget::{
button, column, combo_box, container, horizontal_space, icon,
scrollable, text, text_editor, text_input,
},
Element, Task,
};
use dirs::font_dir;
use iced_video_player::Video;
use tracing::{debug, error};
use crate::{
core::{service_items::ServiceTrait, songs::Song},
ui::{
slide_editor::{self, SlideEditor},
widgets::icon,
},
ui::slide_editor::{self, SlideEditor},
Background, BackgroundKind,
};
@ -29,7 +31,7 @@ use super::presenter::slide_view;
pub struct SongEditor {
pub song: Option<Song>,
title: String,
font_db: fontdb::Database,
font_db: Arc<fontdb::Database>,
fonts: Vec<(fontdb::ID, String)>,
fonts_combo: combo_box::State<String>,
font_sizes: combo_box::State<String>,
@ -70,11 +72,9 @@ pub enum Message {
}
impl SongEditor {
pub fn new() -> Self {
pub fn new(font_db: Arc<fontdb::Database>) -> Self {
let fonts = font_dir();
debug!(?fonts);
let mut font_db = fontdb::Database::new();
font_db.load_system_fonts();
let mut fonts: Vec<(fontdb::ID, String)> = font_db
.faces()
.map(|f| {
@ -131,7 +131,7 @@ impl SongEditor {
audio: PathBuf::new(),
background: None,
video: None,
current_font: iced::font::Font::DEFAULT,
current_font: cosmic::font::default(),
ccli: "8".to_owned(),
slide_state: SlideEditor::default(),
}
@ -271,93 +271,100 @@ impl SongEditor {
let slide_preview = container(self.slide_preview())
.width(Length::FillPortion(2));
let column = column![
let column = column::with_children(vec![
self.toolbar(),
row![
container(self.left_column())
.center_x(Length::FillPortion(2)),
container(slide_preview)
.center_x(Length::FillPortion(3))
],
]
.spacing(15);
]
.into(),
])
.spacing(theme::active().cosmic().space_l());
column.into()
}
fn slide_preview(&self) -> Element<Message> {
if let Some(song) = &self.song {
if let Ok(slides) = song.to_slides() {
let slides = slides
.iter()
.enumerate()
.map(|(index, slide)| {
container(
slide_view(
slide.clone(),
if index == 0 {
&self.video
} else {
&None
},
self.current_font,
false,
false,
)
.map(|_| Message::None),
)
.height(250)
.center_x(Length::Fill)
.padding([0, 20])
.clip(true)
.into()
})
.collect();
scrollable(column(slides).spacing(20))
.height(Length::Fill)
.width(Length::Fill)
.into()
} else {
horizontal_space().into()
}
} else {
horizontal_space().into()
}
// self.slide_state
// .view(Font::with_name("Quicksand Bold"))
// .map(|_s| Message::None)
// .into()
// if let Some(song) = &self.song {
// if let Ok(slides) = song.to_slides() {
// let slides = slides
// .iter()
// .enumerate()
// .map(|(index, slide)| {
// container(
// slide_view(
// slide.clone(),
// if index == 0 {
// &self.video
// } else {
// &None
// },
// self.current_font,
// false,
// false,
// )
// .map(|_| Message::None),
// )
// .height(250)
// .center_x(Length::Fill)
// .padding([0, 20])
// .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()
}
fn left_column(&self) -> Element<Message> {
let title_input = text_input("song", self.title.as_ref())
.on_input(|_| Message::ChangeTitle);
let title_input = text_input("song", &self.title)
.on_input(Message::ChangeTitle)
.label("Song Title");
let author_input = text_input("author", &self.author)
.on_input(|_| Message::ChangeAuthor);
.on_input(Message::ChangeAuthor)
.label("Song Author");
let verse_input = text_input(
"Verse
order",
&self.verse_order,
)
.label("Verse Order")
.on_input(Message::ChangeVerseOrder);
let lyric_title = text("Lyrics");
let lyric_input = column![
let lyric_input = column::with_children(vec![
lyric_title.into(),
text_editor(&self.lyrics)
.on_action(Message::ChangeLyrics)
.height(Length::Fill)
.into(),
]
])
.spacing(5);
column![
column::with_children(vec![
title_input.into(),
author_input.into(),
verse_input.into(),
lyric_input.into(),
]
])
.spacing(25)
.width(Length::FillPortion(2))
.into()
@ -392,20 +399,16 @@ order",
)
},
)
.width(200);
.width(theme::active().cosmic().space_xxl());
let background_selector = button(row!(
let background_selector = button::icon(
icon::from_name("folder-pictures-symbolic").scale(2),
"Background"
))
)
.label("Background")
.tooltip("Select an image or video background")
.on_press(Message::PickBackground)
.padding(10);
let background_selector = tooltip(
background_selector,
"Select an image or video background",
tooltip::Position::FollowCursor,
);
row![
font_selector,
font_size,
@ -442,24 +445,25 @@ order",
impl Default for SongEditor {
fn default() -> Self {
Self::new()
let mut fontdb = fontdb::Database::new();
fontdb.load_system_fonts();
Self::new(Arc::new(fontdb))
}
}
async fn pick_background() -> Result<PathBuf, SongError> {
// let dialog =
// AsyncFileDialog::new().set_title("Choose a background...");
// dialog
let dialog = Dialog::new().title("Choose a background...");
dialog
.open_file()
.await
.map_err(|_| SongError::DialogClosed)
.map(|file| file.url().to_file_path().unwrap())
// rfd::AsyncFileDialog::new()
// .set_title("Choose a background...")
// .pick_file()
// .await
// .map_err(|_| SongError::DialogClosed)
// .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())
// .ok_or(SongError::DialogClosed)
// .map(|file| file.path().to_owned())
}
#[derive(Debug, Clone)]

View file

@ -1,19 +1,29 @@
use std::{
fmt::Display,
hash::{Hash, Hasher},
io::Read,
path::PathBuf,
sync::Arc,
};
use colors_transform::Rgb;
use iced::{
font::{Style, Weight},
widget::{container, svg::Handle, Svg},
Element, Length, Size,
use cosmic::{
iced::{
font::{Style, Weight},
ContentFit, Length, Size,
},
prelude::*,
widget::{container, image::Handle, Image},
};
use tracing::error;
use resvg::{
tiny_skia::{self, Pixmap},
usvg::{fontdb, Tree},
};
use tracing::{debug, error};
use crate::TextAlignment;
#[derive(Clone, Debug, Default, PartialEq)]
#[derive(Clone, Debug, Default)]
pub struct TextSvg {
text: String,
font: Font,
@ -21,7 +31,20 @@ pub struct TextSvg {
stroke: Option<Stroke>,
fill: Color,
alignment: TextAlignment,
handle: Option<Handle>,
pub handle: Option<Handle>,
fontdb: Arc<resvg::usvg::fontdb::Database>,
}
impl PartialEq for TextSvg {
fn eq(&self, other: &Self) -> bool {
self.text == other.text
&& self.font == other.font
&& self.shadow == other.shadow
&& self.stroke == other.stroke
&& self.fill == other.fill
&& self.alignment == other.alignment
&& self.handle == other.handle
}
}
impl Hash for TextSvg {
@ -43,11 +66,34 @@ pub struct Font {
size: u8,
}
impl From<iced::font::Font> for Font {
fn from(value: iced::font::Font) -> Self {
#[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,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Color(Rgb);
impl From<cosmic::font::Font> for Font {
fn from(value: cosmic::font::Font) -> Self {
Self {
name: match value.family {
iced::font::Family::Name(name) => name.to_string(),
cosmic::iced::font::Family::Name(name) => {
name.to_string()
}
_ => "Quicksand Bold".into(),
},
size: 20,
@ -108,9 +154,6 @@ impl Font {
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Color(Rgb);
impl Hash for Color {
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.to_css_hex_string().hash(state);
@ -153,24 +196,6 @@ impl Display for Color {
}
}
#[derive(Clone, Debug, Default, PartialEq, Hash)]
pub struct Shadow {
pub offset_x: i16,
pub offset_y: i16,
pub spread: u16,
pub color: Color,
}
#[derive(Clone, Debug, Default, PartialEq, Hash)]
pub struct Stroke {
size: u16,
color: Color,
}
pub enum Message {
None,
}
impl TextSvg {
pub fn new(text: impl Into<String>) -> Self {
Self {
@ -206,12 +231,33 @@ impl TextSvg {
self
}
pub fn fontdb(mut self, fontdb: Arc<fontdb::Database>) -> Self {
self.fontdb = fontdb;
self
}
pub fn alignment(mut self, alignment: TextAlignment) -> Self {
self.alignment = alignment;
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 {
format!("<filter id=\"shadow\"><feDropShadow dx=\"{}\" dy=\"{}\" stdDeviation=\"{}\" flood-color=\"{}\"/></filter>",
shadow.offset_x,
@ -229,13 +275,13 @@ impl TextSvg {
} else {
"".into()
};
let size = Size::new(640.0, 360.0);
let size = Size::new(1920.0, 1080.0);
let font_size = self.font.size as f32;
let total_lines = self.text.lines().count();
let half_lines = (total_lines / 2) as f32;
let middle_position = size.height / 2.0;
let line_spacing = 10.0;
let text_and_line_spacing =
self.font.size as f32 + line_spacing;
let text_and_line_spacing = font_size + line_spacing;
let starting_y_position =
middle_position - (half_lines * text_and_line_spacing);
@ -259,28 +305,38 @@ impl TextSvg {
size.height,
shadow,
self.font.name,
self.font.size,
font_size,
self.fill, stroke, text);
let handle = Handle::from_memory(
Box::leak(
<std::string::String as Clone>::clone(&final_svg)
.into_boxed_str(),
)
.as_bytes(),
);
debug!("text string built...");
let resvg_tree = Tree::from_data(
&final_svg.as_bytes(),
&resvg::usvg::Options {
fontdb: Arc::clone(&self.fontdb),
..Default::default()
},
)
.expect("Woops mama");
debug!("parsed");
let transform = tiny_skia::Transform::default();
let mut pixmap =
Pixmap::new(size.width as u32, size.height as u32)
.expect("opops");
resvg::render(&resvg_tree, transform, &mut pixmap.as_mut());
let _ = pixmap.save_png(&path);
debug!("rendered");
let handle = Handle::from_path(path);
self.handle = Some(handle);
debug!("stored");
self
}
pub fn view<'a>(&self) -> Element<'a, Message> {
container(
Svg::new(self.handle.clone().unwrap())
.width(Length::Fill)
.height(Length::Fill),
)
.width(Length::Fill)
.height(Length::Fill)
.into()
Image::new(self.handle.clone().unwrap())
.content_fit(ContentFit::Cover)
.width(Length::Fill)
.height(Length::Fill)
.into()
}
fn text_spans(&self) -> Vec<String> {
@ -317,6 +373,26 @@ pub fn color(color: impl AsRef<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)]
mod test {
use pretty_assertions::assert_eq;

View file

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

View file

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

View file

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

View file

@ -1,2 +1 @@
// pub mod slide_text;
pub mod 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::{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 {
text: String,
@ -23,7 +23,7 @@ impl SlideText {
});
let surface =
instance.create_surface(window.clone()).unwrap();
let adapter = iced::iced::wgpu::util::initialize_adapter_from_env_or_default(&instance, Some(&surface))
let adapter = cosmic::iced::wgpu::util::initialize_adapter_from_env_or_default(&instance, Some(&surface))
.await
.expect("Failed to find an appropriate adapter");
let (device, queue) = adapter

View file

@ -1,10 +1,10 @@
(slide :background (image :source "~/pics/frodo.jpg" :fit fill)
(text "This is frodo" :font-size 90))
(text "This is frodo" :font-size 140))
(slide (video :source "~/vids/test/camprules2024.mp4" :fit contain))
(slide (video :source "~/vids/never give up.mkv" :fit contain))
(slide (video :source "~/vids/The promise of Rust.mkv" :fit contain))
(song :id 7 :author "North Point Worship"
:font "Quicksand Bold" :font-size 60
:font "Quicksand" :font-size 140
:shadow "" :stroke ""
:title "Death Was Arrested"
: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"
:font "Quicksand Bold" :font-size 60
:font "Quicksand" :font-size 140
:title "Death Was Arrested"
:background (image :source "~/nc/tfc/openlp/CMG - Bright Mountains 01.jpg" :fit cover)
:text-alignment center

View file

@ -17,6 +17,11 @@ Actually, what if we just made the svg at load/creation time and stored it in th
** SVG performs badly
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 Build library to see all available songs, images, videos, presentations, and slides