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]
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"
miette = { version = "7.2.0", features = ["fancy"] }
pretty_assertions = "1.4.1"
@ -29,7 +29,8 @@ gstreamer-app = "0.23.3"
# cosmic-time = "0.2.0"
url = "2"
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"
# rfd = { version = "0.12.1", features = ["xdg-portal"], default-features = false }
@ -39,7 +40,8 @@ branch = "cosmic"
features = ["wgpu"]
[profile.dev]
opt-level = 0
opt-level = 3
[profile.release]
opt-level = 3
debug = true

76
flake.lock generated
View file

@ -6,11 +6,33 @@
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1745303921,
"narHash": "sha256-zYucemS2QvJUR5GKJ/u3eZAoe82AKhcxMtNVZDERXsw=",
"lastModified": 1754980813,
"narHash": "sha256-Wr9ei2V4rfr3HR5eJUA7pjMIrHH5o4DtWazQC5UwxHA=",
"owner": "nix-community",
"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"
},
"original": {
@ -39,14 +61,15 @@
},
"naersk": {
"inputs": {
"fenix": "fenix_2",
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1743800763,
"narHash": "sha256-YFKV+fxEpMgP5VsUcM6Il28lI0NlpM7+oB1XxbBAYCw=",
"lastModified": 1752689277,
"narHash": "sha256-uldUBFkZe/E7qbvxa3mH1ItrWZyT6w1dBKJQF/3ZSsc=",
"owner": "nix-community",
"repo": "naersk",
"rev": "ed0232117731a4c19d3ee93aa0c382a8fe754b01",
"rev": "0e72363d0938b0208d6c646d10649164c43f4d64",
"type": "github"
},
"original": {
@ -57,11 +80,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1744932701,
"narHash": "sha256-fusHbZCyv126cyArUwwKrLdCkgVAIaa/fQJYFlCEqiU=",
"lastModified": 1754725699,
"narHash": "sha256-iAcj9T/Y+3DBy2J0N+yF9XQQQ8IEb5swLFzs23CdP88=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "b024ced1aac25639f8ca8fdfc2f8c4fbd66c48ef",
"rev": "85dbfc7aaf52ecb755f87e577ddbe6dbbdbc1054",
"type": "github"
},
"original": {
@ -73,11 +96,11 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1744868846,
"narHash": "sha256-5RJTdUHDmj12Qsv7XOhuospjAjATNiTMElplWnJE9Hs=",
"lastModified": 1752077645,
"narHash": "sha256-HM791ZQtXV93xtCY+ZxG1REzhQenSQO020cu6rHtAPk=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "ebe4301cbd8f81c4f8d3244b3632338bbeb6d49c",
"rev": "be9e214982e20b8310878ac2baa063a961c1bdf6",
"type": "github"
},
"original": {
@ -89,11 +112,11 @@
},
"nixpkgs_3": {
"locked": {
"lastModified": 1745234285,
"narHash": "sha256-GfpyMzxwkfgRVN0cTGQSkTC0OHhEkv3Jf6Tcjm//qZ0=",
"lastModified": 1754725699,
"narHash": "sha256-iAcj9T/Y+3DBy2J0N+yF9XQQQ8IEb5swLFzs23CdP88=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "c11863f1e964833214b767f4a369c6e6a7aba141",
"rev": "85dbfc7aaf52ecb755f87e577ddbe6dbbdbc1054",
"type": "github"
},
"original": {
@ -114,11 +137,28 @@
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1745247864,
"narHash": "sha256-QA1Ba8Flz5K+0GbG03HwiX9t46mh/jjKgwavbuKtwMg=",
"lastModified": 1754926538,
"narHash": "sha256-fuHLsvM5z5/5ia3yL0/mr472wXnxSrtXECa+pspQchA=",
"owner": "rust-lang",
"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"
},
"original": {

View file

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

View file

@ -72,7 +72,10 @@ fn main() -> Result<()> {
let settings;
if args.ui {
debug!("main view");
settings = Settings::default().debug(false).is_daemon(true);
settings = Settings::default()
.debug(false)
.is_daemon(true)
.transparent(true);
} else {
debug!("window view");
settings = Settings::default()
@ -105,6 +108,7 @@ struct App {
library_width: f32,
editor_mode: Option<EditorMode>,
song_editor: SongEditor,
searching: bool,
}
#[derive(Debug, Clone)]
@ -126,6 +130,7 @@ enum Message {
None,
DndLeave(Entity),
EditorToggle(bool),
SearchFocus,
}
const HEADER_SPACE: u16 = 6;
@ -203,6 +208,7 @@ impl cosmic::Application for App {
library_width: 60.0,
editor_mode: None,
song_editor,
searching: false,
};
let mut batch = vec![];
@ -243,18 +249,18 @@ impl cosmic::Application for App {
debug!("left");
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| {
debug!("dropped");
cosmic::Action::App(Message::DndDrop(
entity, data, action,
))
})
.drag_id(DragId::new())
.on_context(|id| {
cosmic::Action::Cosmic(
cosmic::app::Action::NavBarContext(id),
)
})
.context_menu(None)
.into_container()
// 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>> {
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 text = if self.presentation_open {
text::body("End Presentation")
@ -295,7 +320,7 @@ impl cosmic::Application for App {
text::body("Present")
};
vec![
let row = row![
tooltip(
button::custom(
row!(
@ -318,9 +343,7 @@ impl cosmic::Application for App {
)),
"Enter Edit Mode",
TPosition::Bottom,
)
.into(),
horizontal_space().width(HEADER_SPACE).into(),
),
tooltip(
button::custom(
row!(
@ -351,33 +374,7 @@ impl cosmic::Application for App {
}),
"Start Presentation",
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(
button::custom(
row!(
@ -399,9 +396,10 @@ impl cosmic::Application for App {
"Open Library",
TPosition::Bottom,
)
.into(),
horizontal_space().width(HEADER_SPACE).into(),
]
.spacing(HEADER_SPACE)
.into();
vec![row]
}
fn footer(&self) -> Option<Element<Self::Message>> {
@ -466,6 +464,14 @@ impl cosmic::Application for App {
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> {
match message {
Message::Key(key, modifiers) => {
@ -587,16 +593,16 @@ impl cosmic::Application for App {
Message::WindowOpened(id, _) => {
debug!(?id, "Window opened");
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?")
{
self.presentation_open = true;
if let Some(video) = &mut self.presenter.video {
video.set_muted(false);
}
window::change_mode(id, Mode::Fullscreen)
} else {
Task::none()
}
|| 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;
if let Some(video) = &mut self.presenter.video {
video.set_muted(false);
}
window::change_mode(id, Mode::Fullscreen)
} else {
Task::none()
}
}
Message::WindowClosed(id) => {
warn!("Closing window: {id}");
@ -658,6 +664,10 @@ impl cosmic::Application for App {
}
Task::none()
}
Message::SearchFocus => {
self.searching = true;
Task::none()
}
}
}

View file

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

View file

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

View file

@ -7,7 +7,7 @@ use colors_transform::Rgb;
use cosmic::{
iced::{
font::{Style, Weight},
Length,
Length, Size,
},
prelude::*,
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 {
fn from(value: &str) -> Self {
Self {
@ -168,8 +177,9 @@ pub enum Message {
}
impl TextSvg {
pub fn new() -> Self {
pub fn new(text: impl Into<String>) -> Self {
Self {
text: text.into(),
..Default::default()
}
}
@ -206,7 +216,7 @@ impl TextSvg {
self
}
pub fn view<'a>(self) -> Element<'a, Message> {
pub fn build(mut self) -> Self {
let shadow = if let Some(shadow) = &self.shadow {
format!("<filter id=\"shadow\"><feDropShadow dx=\"{}\" dy=\"{}\" stdDeviation=\"{}\" flood-color=\"{}\"/></filter>",
shadow.offset_x,
@ -224,38 +234,58 @@ impl TextSvg {
} else {
"".into()
};
container(
responsive(move |s| {
let total_lines = self.text.lines().count();
let half_lines = (total_lines / 2) as f32;
let middle_position = s.height / 2.0;
let line_spacing = 10.0;
let text_and_line_spacing = self.font.size as f32 + line_spacing;
let starting_y_position = middle_position - (half_lines * text_and_line_spacing);
let size = Size::new(640.0, 360.0);
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 starting_y_position =
middle_position - (half_lines * text_and_line_spacing);
let text_pieces: Vec<String> = self.text.lines()
.enumerate()
.map(|(index, text)| {
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 text_pieces: Vec<String> = self
.text
.lines()
.enumerate()
.map(|(index, text)| {
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>",
s.width,
s.height,
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>",
size.width,
size.height,
shadow,
self.font.name,
self.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(),
);
self.handle = Some(handle);
self
}
// debug!(final_svg);
Svg::new(Handle::from_memory(
Box::leak(<std::string::String as Clone>::clone(&final_svg).into_boxed_str()).as_bytes(),
))
.width(Length::Fill)
.height(Length::Fill)
.into()
})).width(Length::Fill).height(Length::Fill).into()
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()
}
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)
}
}