updating and fixing some small performance issues
Some checks are pending
/ test (push) Waiting to run

This commit is contained in:
Chris Cochrun 2025-08-13 13:51:39 -05:00
parent fd94f1dfa6
commit a06890d9e1
10 changed files with 911 additions and 712 deletions

1001
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -8,7 +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", "winit_wgpu", "tokio", "rfd", "dbus-config", "a11y", "wgpu", "multi-window"] } 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"
@ -29,7 +29,8 @@ gstreamer-app = "0.23.3"
# cosmic-time = "0.2.0" # cosmic-time = "0.2.0"
url = "2" url = "2"
colors-transform = "0.2.11" colors-transform = "0.2.11"
femtovg = { version = "0.16.0", features = ["wgpu"] } # femtovg = { version = "0.16.0", features = ["wgpu"] }
# 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 }
@ -39,7 +40,8 @@ branch = "cosmic"
features = ["wgpu"] features = ["wgpu"]
[profile.dev] [profile.dev]
opt-level = 0 opt-level = 3
[profile.release] [profile.release]
opt-level = 3 opt-level = 3
debug = true

76
flake.lock generated
View file

@ -6,11 +6,33 @@
"rust-analyzer-src": "rust-analyzer-src" "rust-analyzer-src": "rust-analyzer-src"
}, },
"locked": { "locked": {
"lastModified": 1745303921, "lastModified": 1754980813,
"narHash": "sha256-zYucemS2QvJUR5GKJ/u3eZAoe82AKhcxMtNVZDERXsw=", "narHash": "sha256-Wr9ei2V4rfr3HR5eJUA7pjMIrHH5o4DtWazQC5UwxHA=",
"owner": "nix-community", "owner": "nix-community",
"repo": "fenix", "repo": "fenix",
"rev": "14850d5984f3696a2972f85f19085e5fb46daa95", "rev": "a1ce805b08279ee4e697b47aa3aa28fe2b335de6",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "fenix",
"type": "github"
}
},
"fenix_2": {
"inputs": {
"nixpkgs": [
"naersk",
"nixpkgs"
],
"rust-analyzer-src": "rust-analyzer-src_2"
},
"locked": {
"lastModified": 1752475459,
"narHash": "sha256-z6QEu4ZFuHiqdOPbYss4/Q8B0BFhacR8ts6jO/F/aOU=",
"owner": "nix-community",
"repo": "fenix",
"rev": "bf0d6f70f4c9a9cf8845f992105652173f4b617f",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -39,14 +61,15 @@
}, },
"naersk": { "naersk": {
"inputs": { "inputs": {
"fenix": "fenix_2",
"nixpkgs": "nixpkgs_2" "nixpkgs": "nixpkgs_2"
}, },
"locked": { "locked": {
"lastModified": 1743800763, "lastModified": 1752689277,
"narHash": "sha256-YFKV+fxEpMgP5VsUcM6Il28lI0NlpM7+oB1XxbBAYCw=", "narHash": "sha256-uldUBFkZe/E7qbvxa3mH1ItrWZyT6w1dBKJQF/3ZSsc=",
"owner": "nix-community", "owner": "nix-community",
"repo": "naersk", "repo": "naersk",
"rev": "ed0232117731a4c19d3ee93aa0c382a8fe754b01", "rev": "0e72363d0938b0208d6c646d10649164c43f4d64",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -57,11 +80,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1744932701, "lastModified": 1754725699,
"narHash": "sha256-fusHbZCyv126cyArUwwKrLdCkgVAIaa/fQJYFlCEqiU=", "narHash": "sha256-iAcj9T/Y+3DBy2J0N+yF9XQQQ8IEb5swLFzs23CdP88=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "b024ced1aac25639f8ca8fdfc2f8c4fbd66c48ef", "rev": "85dbfc7aaf52ecb755f87e577ddbe6dbbdbc1054",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -73,11 +96,11 @@
}, },
"nixpkgs_2": { "nixpkgs_2": {
"locked": { "locked": {
"lastModified": 1744868846, "lastModified": 1752077645,
"narHash": "sha256-5RJTdUHDmj12Qsv7XOhuospjAjATNiTMElplWnJE9Hs=", "narHash": "sha256-HM791ZQtXV93xtCY+ZxG1REzhQenSQO020cu6rHtAPk=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "ebe4301cbd8f81c4f8d3244b3632338bbeb6d49c", "rev": "be9e214982e20b8310878ac2baa063a961c1bdf6",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -89,11 +112,11 @@
}, },
"nixpkgs_3": { "nixpkgs_3": {
"locked": { "locked": {
"lastModified": 1745234285, "lastModified": 1754725699,
"narHash": "sha256-GfpyMzxwkfgRVN0cTGQSkTC0OHhEkv3Jf6Tcjm//qZ0=", "narHash": "sha256-iAcj9T/Y+3DBy2J0N+yF9XQQQ8IEb5swLFzs23CdP88=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "c11863f1e964833214b767f4a369c6e6a7aba141", "rev": "85dbfc7aaf52ecb755f87e577ddbe6dbbdbc1054",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -114,11 +137,28 @@
"rust-analyzer-src": { "rust-analyzer-src": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1745247864, "lastModified": 1754926538,
"narHash": "sha256-QA1Ba8Flz5K+0GbG03HwiX9t46mh/jjKgwavbuKtwMg=", "narHash": "sha256-fuHLsvM5z5/5ia3yL0/mr472wXnxSrtXECa+pspQchA=",
"owner": "rust-lang", "owner": "rust-lang",
"repo": "rust-analyzer", "repo": "rust-analyzer",
"rev": "31dbec70c68e97060916d4754c687a3e93c2440f", "rev": "9db05508ed08a4c952017769b45b57c4ad505872",
"type": "github"
},
"original": {
"owner": "rust-lang",
"ref": "nightly",
"repo": "rust-analyzer",
"type": "github"
}
},
"rust-analyzer-src_2": {
"flake": false,
"locked": {
"lastModified": 1752428706,
"narHash": "sha256-EJcdxw3aXfP8Ex1Nm3s0awyH9egQvB2Gu+QEnJn2Sfg=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "591e3b7624be97e4443ea7b5542c191311aa141d",
"type": "github" "type": "github"
}, },
"original": { "original": {

View file

@ -9,6 +9,8 @@ use std::{
}; };
use tracing::error; use tracing::error;
use crate::ui::text_svg::{self, TextSvg};
use super::songs::Song; use super::songs::Song;
#[derive( #[derive(
@ -234,6 +236,8 @@ pub struct Slide {
video_loop: bool, video_loop: bool,
video_start_time: f32, video_start_time: f32,
video_end_time: f32, video_end_time: f32,
#[serde(skip)]
pub text_svg: TextSvg,
} }
impl From<&Slide> for Value { impl From<&Slide> for Value {
@ -498,6 +502,8 @@ pub struct SlideBuilder {
video_loop: Option<bool>, video_loop: Option<bool>,
video_start_time: Option<f32>, video_start_time: Option<f32>,
video_end_time: Option<f32>, video_end_time: Option<f32>,
#[serde(skip)]
text_svg: Option<TextSvg>,
} }
impl SlideBuilder { impl SlideBuilder {
@ -571,6 +577,14 @@ impl SlideBuilder {
self self
} }
pub(crate) fn text_svg(
mut self,
text_svg: impl Into<TextSvg>,
) -> Self {
let _ = self.text_svg.insert(text_svg.into());
self
}
pub(crate) fn build(self) -> Result<Slide> { pub(crate) fn build(self) -> Result<Slide> {
let Some(background) = self.background else { let Some(background) = self.background else {
return Err(miette!("No background")); return Err(miette!("No background"));
@ -596,18 +610,45 @@ 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"));
}; };
Ok(Slide { if let Some(text_svg) = self.text_svg {
background, Ok(Slide {
text, background,
font, text,
font_size, font,
text_alignment, font_size,
audio: self.audio, text_alignment,
video_loop, audio: self.audio,
video_start_time, video_loop,
video_end_time, video_start_time,
..Default::default() 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()
})
}
} }
} }

View file

@ -72,7 +72,10 @@ fn main() -> Result<()> {
let settings; let settings;
if args.ui { if args.ui {
debug!("main view"); debug!("main view");
settings = Settings::default().debug(false).is_daemon(true); settings = Settings::default()
.debug(false)
.is_daemon(true)
.transparent(true);
} else { } else {
debug!("window view"); debug!("window view");
settings = Settings::default() settings = Settings::default()
@ -105,6 +108,7 @@ struct App {
library_width: f32, library_width: f32,
editor_mode: Option<EditorMode>, editor_mode: Option<EditorMode>,
song_editor: SongEditor, song_editor: SongEditor,
searching: bool,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -126,6 +130,7 @@ enum Message {
None, None,
DndLeave(Entity), DndLeave(Entity),
EditorToggle(bool), EditorToggle(bool),
SearchFocus,
} }
const HEADER_SPACE: u16 = 6; const HEADER_SPACE: u16 = 6;
@ -203,6 +208,7 @@ impl cosmic::Application for App {
library_width: 60.0, library_width: 60.0,
editor_mode: None, editor_mode: None,
song_editor, song_editor,
searching: false,
}; };
let mut batch = vec![]; let mut batch = vec![];
@ -243,18 +249,18 @@ impl cosmic::Application for App {
debug!("left"); debug!("left");
cosmic::Action::App(Message::DndLeave(entity)) cosmic::Action::App(Message::DndLeave(entity))
}) })
.drag_id(DragId::new())
.on_context(|id| {
cosmic::Action::Cosmic(
cosmic::app::Action::NavBarContext(id),
)
})
.on_dnd_drop::<ServiceItem>(|entity, data, action| { .on_dnd_drop::<ServiceItem>(|entity, data, action| {
debug!("dropped"); debug!("dropped");
cosmic::Action::App(Message::DndDrop( cosmic::Action::App(Message::DndDrop(
entity, data, action, entity, data, action,
)) ))
}) })
.drag_id(DragId::new())
.on_context(|id| {
cosmic::Action::Cosmic(
cosmic::app::Action::NavBarContext(id),
)
})
.context_menu(None) .context_menu(None)
.into_container() .into_container()
// XXX both must be shrink to avoid flex layout from ignoring it // XXX both must be shrink to avoid flex layout from ignoring it
@ -288,6 +294,25 @@ impl cosmic::Application for App {
} }
fn header_start(&self) -> Vec<Element<Self::Message>> { fn header_start(&self) -> Vec<Element<Self::Message>> {
vec![]
}
fn header_center(&self) -> Vec<Element<Self::Message>> {
vec![search_input("Search...", "")
.on_input(|_| Message::None)
.on_submit(|_| Message::None)
.on_focus(Message::SearchFocus)
.width(1200)
.into()]
}
fn header_end(&self) -> Vec<Element<Self::Message>> {
// let editor_toggle = toggler(self.editor_mode.is_some())
// .label("Editor")
// .spacing(10)
// .width(Length::Shrink)
// .on_toggle(Message::EditorToggle);
let presenter_window = self.windows.get(1); let presenter_window = self.windows.get(1);
let text = if self.presentation_open { let text = if self.presentation_open {
text::body("End Presentation") text::body("End Presentation")
@ -295,7 +320,7 @@ impl cosmic::Application for App {
text::body("Present") text::body("Present")
}; };
vec![ let row = row![
tooltip( tooltip(
button::custom( button::custom(
row!( row!(
@ -318,9 +343,7 @@ impl cosmic::Application for App {
)), )),
"Enter Edit Mode", "Enter Edit Mode",
TPosition::Bottom, TPosition::Bottom,
) ),
.into(),
horizontal_space().width(HEADER_SPACE).into(),
tooltip( tooltip(
button::custom( button::custom(
row!( row!(
@ -351,33 +374,7 @@ impl cosmic::Application for App {
}), }),
"Start Presentation", "Start Presentation",
TPosition::Bottom, TPosition::Bottom,
) ),
.into(),
horizontal_space().width(HEADER_SPACE).into(),
]
}
fn header_center(&self) -> Vec<Element<Self::Message>> {
vec![search_input("Search...", "")
.on_input(|_| Message::None)
.on_submit(|_| Message::None)
.width(300)
.into()]
}
fn header_end(&self) -> Vec<Element<Self::Message>> {
// let editor_toggle = toggler(self.editor_mode.is_some())
// .label("Editor")
// .spacing(10)
// .width(Length::Shrink)
// .on_toggle(Message::EditorToggle);
let presenter_window = self.windows.get(1);
let text = if self.presentation_open {
text::body("End Presentation")
} else {
text::body("Present")
};
vec![
tooltip( tooltip(
button::custom( button::custom(
row!( row!(
@ -399,9 +396,10 @@ impl cosmic::Application for App {
"Open Library", "Open Library",
TPosition::Bottom, TPosition::Bottom,
) )
.into(),
horizontal_space().width(HEADER_SPACE).into(),
] ]
.spacing(HEADER_SPACE)
.into();
vec![row]
} }
fn footer(&self) -> Option<Element<Self::Message>> { fn footer(&self) -> Option<Element<Self::Message>> {
@ -466,6 +464,14 @@ impl cosmic::Application for App {
None None
} }
fn dialog(&self) -> Option<Element<'_, Self::Message>> {
if self.searching {
Some(text("hello").into())
} else {
None
}
}
fn update(&mut self, message: Message) -> Task<Message> { fn update(&mut self, message: Message) -> Task<Message> {
match message { match message {
Message::Key(key, modifiers) => { Message::Key(key, modifiers) => {
@ -587,16 +593,16 @@ impl cosmic::Application for App {
Message::WindowOpened(id, _) => { Message::WindowOpened(id, _) => {
debug!(?id, "Window opened"); debug!(?id, "Window opened");
if self.cli_mode if self.cli_mode
|| id > self.core.main_window_id().expect("Cosmic core seems to be missing a main window, was this started in cli mode?") || id > self.core.main_window_id().expect("Cosmic core seems to be missing a main window, was this started in cli mode?")
{ {
self.presentation_open = true; self.presentation_open = true;
if let Some(video) = &mut self.presenter.video { if let Some(video) = &mut self.presenter.video {
video.set_muted(false); video.set_muted(false);
} }
window::change_mode(id, Mode::Fullscreen) window::change_mode(id, Mode::Fullscreen)
} else { } else {
Task::none() Task::none()
} }
} }
Message::WindowClosed(id) => { Message::WindowClosed(id) => {
warn!("Closing window: {id}"); warn!("Closing window: {id}");
@ -658,6 +664,10 @@ impl cosmic::Application for App {
} }
Task::none() Task::none()
} }
Message::SearchFocus => {
self.searching = true;
Task::none()
}
} }
} }

View file

@ -6,6 +6,7 @@ pub mod presenter;
pub mod song_editor; pub mod song_editor;
pub mod text_svg; pub mod text_svg;
pub mod video; pub mod video;
pub mod widgets;
pub enum EditorMode { pub enum EditorMode {
Song, Song,

View file

@ -33,6 +33,7 @@ use url::Url;
use crate::{ use crate::{
core::{service_items::ServiceItemModel, slide::Slide}, core::{service_items::ServiceItemModel, slide::Slide},
ui::text_svg::{self, Font as SvgFont}, ui::text_svg::{self, Font as SvgFont},
// ui::widgets::slide_text,
BackgroundKind, BackgroundKind,
}; };
@ -88,7 +89,7 @@ impl Presenter {
gst::init().into_diagnostic()?; gst::init().into_diagnostic()?;
let pipeline = format!( let pipeline = format!(
r#"playbin uri="{}" video-sink="videoscale ! videoconvert ! appsink name=iced_video drop=true caps=video/x-raw,format=NV12,pixel-aspect-ratio=1/1""#, r#"playbin uri="{}" video-sink="videoscale ! videoconvert ! appsink name=lumina_video drop=true caps=video/x-raw,format=NV12,pixel-aspect-ratio=1/1""#,
url.as_str() url.as_str()
); );
@ -108,13 +109,14 @@ impl Presenter {
.unwrap() .unwrap()
.downcast::<gst::Bin>() .downcast::<gst::Bin>()
.unwrap(); .unwrap();
let video_sink = bin.by_name("iced_video").unwrap(); let video_sink = bin.by_name("lumina_video").unwrap();
let video_sink = let video_sink =
video_sink.downcast::<gst_app::AppSink>().unwrap(); video_sink.downcast::<gst_app::AppSink>().unwrap();
let result = let result =
Video::from_gst_pipeline(pipeline, video_sink, None); Video::from_gst_pipeline(pipeline, video_sink, None);
result.into_diagnostic() result.into_diagnostic()
} }
pub fn with_items(items: ServiceItemModel) -> Self { pub fn with_items(items: ServiceItemModel) -> Self {
let slides = items.to_slides().unwrap_or_default(); let slides = items.to_slides().unwrap_or_default();
let video = { let video = {
@ -545,75 +547,89 @@ pub(crate) fn slide_view(
) -> Element<'_, Message> { ) -> Element<'_, Message> {
responsive(move |size| { responsive(move |size| {
let width = size.height * 16.0 / 9.0; let width = size.height * 16.0 / 9.0;
let font_size = scale_font(slide.font_size() as f32, width);
let slide_text = slide.text(); let slide_text = slide.text();
// SVG based
// let font = SvgFont::from(font).size(font_size.floor() as u8); // let font = SvgFont::from(font).size(font_size.floor() as u8);
// let text = text_svg::TextSvg::new() let text_container = if delegate {
// .text(&slide_text) // text widget based
// .fill("#fff") let font_size =
// .shadow(text_svg::shadow(2, 2, 5, "#000000")) scale_font(slide.font_size() as f32, width);
// .stroke(text_svg::stroke(3, "#000")) let lines = slide_text.lines();
// .font(font) let text: Vec<Element<Message>> = lines
// .view() .map(|t| {
// .map(|m| Message::None); rich_text([span(format!("{}\n", t))
.background(
// text widget based Background::Color(Color::BLACK)
let lines = slide_text.lines(); .scale_alpha(0.4),
let text: Vec<Element<Message>> = lines )
.map(|t| { .border(border::rounded(10))
rich_text([span(format!("{}\n", t)) .padding(10)])
.background( .size(font_size)
Background::Color(Color::BLACK) .font(font)
.scale_alpha(0.4), .center()
) .into()
.border(border::rounded(10)) // let chars: Vec<Span> = t
.padding(10)]) // .chars()
.size(font_size) // .map(|c| -> Span {
.font(font) // let character: String = format!("{}/n", c);
.center() // span(character)
.into() // .size(font_size)
// let chars: Vec<Span> = t // .font(font)
// .chars() // .background(
// .map(|c| -> Span { // Background::Color(Color::BLACK)
// let character: String = format!("{}/n", c); // .scale_alpha(0.4),
// span(character) // )
// .size(font_size) // .border(border::rounded(10))
// .font(font) // .padding(10)
// .background( })
// Background::Color(Color::BLACK) .collect();
// .scale_alpha(0.4), let text = Column::with_children(text).spacing(26);
// ) Container::new(text)
// .border(border::rounded(10)) .center(Length::Fill)
// .padding(10) .align_x(Horizontal::Left)
}) } else {
.collect(); // SVG based
let text = Column::with_children(text).spacing(26); let text = slide.text_svg.view().map(|m| Message::None);
Container::new(text)
// let lines = slide_text.lines(); .center(Length::Fill)
// let stroke_text: Vec<Element<Message>> = lines .align_x(Horizontal::Left)
// .map(|t| { // text widget based
// let mut stroke_font = font.clone(); // let font_size =
// stroke_font.stretch = Stretch::Condensed; // scale_font(slide.font_size() as f32, width);
// stroke_font.weight = Weight::Bold; // let lines = slide_text.lines();
// rich_text([span(format!("{}\n", t)) // let text: Vec<Element<Message>> = lines
// .size(font_size + 0.3) // .map(|t| {
// .font(stroke_font) // rich_text([span(format!("{}\n", t))
// .color(Color::BLACK) // .background(
// .border(border::rounded(10)) // Background::Color(Color::BLACK)
// .padding(10)]) // .scale_alpha(0.4),
// .center() // )
// .into() // .border(border::rounded(10))
// }) // .padding(10)])
// .collect(); // .size(font_size)
// let stroke_text = // .font(font)
// Column::with_children(stroke_text).spacing(26); // .center()
// .into()
//Next // // let chars: Vec<Span> = t
let text_container = Container::new(text) // // .chars()
.center(Length::Fill) // // .map(|c| -> Span {
.align_x(Horizontal::Left); // // 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)
@ -648,11 +664,10 @@ pub(crate) fn slide_view(
Color::BLACK, Color::BLACK,
)) ))
}) })
.center_x(width) .center(Length::Fill)
.center_y(size.height)
.clip(true) .clip(true)
.width(Length::Fill) .width(width)
.height(Length::Fill) .height(size.height)
} else if let Some(video) = &video { } else if let Some(video) = &video {
Container::new( Container::new(
VideoPlayer::new(video) VideoPlayer::new(video)

View file

@ -7,7 +7,7 @@ use colors_transform::Rgb;
use cosmic::{ use cosmic::{
iced::{ iced::{
font::{Style, Weight}, font::{Style, Weight},
Length, Length, Size,
}, },
prelude::*, prelude::*,
widget::{container, lazy, responsive, svg::Handle, Svg}, widget::{container, lazy, responsive, svg::Handle, Svg},
@ -61,6 +61,15 @@ impl From<cosmic::font::Font> for Font {
} }
} }
impl From<String> for Font {
fn from(value: String) -> Self {
Self {
name: value,
..Default::default()
}
}
}
impl From<&str> for Font { impl From<&str> for Font {
fn from(value: &str) -> Self { fn from(value: &str) -> Self {
Self { Self {
@ -168,8 +177,9 @@ pub enum Message {
} }
impl TextSvg { impl TextSvg {
pub fn new() -> Self { pub fn new(text: impl Into<String>) -> Self {
Self { Self {
text: text.into(),
..Default::default() ..Default::default()
} }
} }
@ -206,7 +216,7 @@ impl TextSvg {
self self
} }
pub fn view<'a>(self) -> Element<'a, Message> { pub fn build(mut self) -> 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,
@ -224,38 +234,58 @@ impl TextSvg {
} else { } else {
"".into() "".into()
}; };
container( let size = Size::new(640.0, 360.0);
responsive(move |s| { 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 = s.height / 2.0; let line_spacing = 10.0;
let line_spacing = 10.0; let text_and_line_spacing =
let text_and_line_spacing = self.font.size as f32 + line_spacing; self.font.size as f32 + line_spacing;
let starting_y_position = middle_position - (half_lines * text_and_line_spacing); let starting_y_position =
middle_position - (half_lines * text_and_line_spacing);
let text_pieces: Vec<String> = self.text.lines() let text_pieces: Vec<String> = self
.enumerate() .text
.map(|(index, text)| { .lines()
format!("<tspan x=\"50%\" y=\"{}\">{}</tspan>", starting_y_position + (index as f32 * text_and_line_spacing), text) .enumerate()
}).collect(); .map(|(index, text)| {
let text: String = text_pieces.join("\n"); format!(
"<tspan x=\"50%\" y=\"{}\">{}</tspan>",
starting_y_position
+ (index as f32 * text_and_line_spacing),
text
)
})
.collect();
let text: String = text_pieces.join("\n");
let final_svg = format!("<svg viewBox=\"0 0 {} {}\" xmlns=\"http://www.w3.org/2000/svg\"><defs>{}</defs><text x=\"50%\" y=\"50%\" dominant-baseline=\"middle\" text-anchor=\"middle\" font-weight=\"bold\" font-family=\"{}\" font-size=\"{}\" fill=\"{}\" {} style=\"filter:url(#shadow);\">{}</text></svg>", let final_svg = format!("<svg viewBox=\"0 0 {} {}\" xmlns=\"http://www.w3.org/2000/svg\"><defs>{}</defs><text x=\"50%\" y=\"50%\" dominant-baseline=\"middle\" text-anchor=\"middle\" font-weight=\"bold\" font-family=\"{}\" font-size=\"{}\" fill=\"{}\" {} style=\"filter:url(#shadow);\">{}</text></svg>",
s.width, size.width,
s.height, size.height,
shadow, shadow,
self.font.name, self.font.name,
self.font.size, self.font.size,
self.fill, stroke, text); self.fill, stroke, text);
let handle = Handle::from_memory(
Box::leak(
<std::string::String as Clone>::clone(&final_svg)
.into_boxed_str(),
)
.as_bytes(),
);
self.handle = Some(handle);
self
}
// debug!(final_svg); pub fn view<'a>(&self) -> Element<'a, Message> {
Svg::new(Handle::from_memory( container(
Box::leak(<std::string::String as Clone>::clone(&final_svg).into_boxed_str()).as_bytes(), Svg::new(self.handle.clone().unwrap())
)) .width(Length::Fill)
.width(Length::Fill) .height(Length::Fill),
.height(Length::Fill) )
.into() .width(Length::Fill)
})).width(Length::Fill).height(Length::Fill).into() .height(Length::Fill)
.into()
} }
fn text_spans(&self) -> Vec<String> { fn text_spans(&self) -> Vec<String> {

1
src/ui/widgets/mod.rs Normal file
View file

@ -0,0 +1 @@
// pub mod slide_text;

View file

@ -0,0 +1,116 @@
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};
pub struct SlideText {
text: String,
font_size: f32,
canvas: Canvas<WGPURenderer>,
}
impl SlideText {
pub async fn new(text: &str) -> Self {
let backends = wgpu::Backends::PRIMARY;
let instance =
wgpu::Instance::new(wgpu::InstanceDescriptor {
backends,
..Default::default()
});
let surface =
instance.create_surface(window.clone()).unwrap();
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
.request_device(
&wgpu::DeviceDescriptor {
label: None,
required_features: adapter.features(),
required_limits: wgpu::Limits::default(),
memory_hints: wgpu::MemoryHints::Performance,
},
None,
)
.await
.expect("failed to device it");
let renderer = WGPURenderer::new(device, queue);
let canvas =
Canvas::new_with_text_context(renderer, text_context)
.expect("oops femtovg");
Self {
text: text.to_owned(),
font_size: 50.0,
canvas,
}
}
}
fn get_canvas(text_context: TextContext) -> Canvas {
let renderer = WGPURenderer::new(device, queue);
Canvas::new_with_text_context(renderer, text_context)
.expect("oops femtovg")
}
pub fn slide_text(text: &str) -> SlideText {
SlideText::new(text)
}
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for SlideText
where
Renderer: renderer::Renderer,
{
fn size(&self) -> Size<Length> {
Size {
width: Length::Shrink,
height: Length::Shrink,
}
}
fn layout(
&self,
_tree: &mut widget::Tree,
_renderer: &Renderer,
_limits: &layout::Limits,
) -> layout::Node {
layout::Node::new(Size::new(
self.font_size * 2.0,
self.font_size * 2.0,
))
}
fn draw(
&self,
_state: &widget::Tree,
renderer: &mut Renderer,
_theme: &Theme,
_style: &renderer::Style,
layout: Layout<'_>,
_cursor: mouse::Cursor,
_viewport: &Rectangle,
) {
renderer.fill_quad(
renderer::Quad {
bounds: layout.bounds(),
border: border::rounded(self.font_size),
..renderer::Quad::default()
},
Color::BLACK,
);
}
}
impl<Message, Theme, Renderer> From<SlideText>
for Element<'_, Message, Theme, Renderer>
where
Renderer: renderer::Renderer,
{
fn from(circle: SlideText) -> Self {
Self::new(circle)
}
}