Compare commits

...
Sign in to create a new pull request.

4 commits

Author SHA1 Message Date
80a9b48ae9 my fork of iced_video_player
Some checks failed
/ test (push) Has been cancelled
2025-08-27 09:02:46 -05:00
1861f357a8 well crap
Some checks are pending
/ test (push) Waiting to run
2025-08-26 15:25:04 -05:00
4ae6a9a9a7 ohh
Some checks are pending
/ test (push) Waiting to run
2025-08-26 09:47:35 -05:00
c886d18134 beginning the switch... lots to go yet
Some checks are pending
/ test (push) Waiting to run
2025-08-26 09:44:06 -05:00
19 changed files with 2170 additions and 2185 deletions

2504
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", "desktop", "winit_wgpu", "winit_tokio", "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"
@ -37,12 +37,19 @@ rayon = "1.11.0"
# 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 }
rfd = { version = "0.12.1", features = ["xdg-portal"], default-features = false }
derive_setters = "0.1.8"
freedesktop-icons = "0.4.0"
[dependencies.iced]
git = "https://github.com/iced-rs/iced"
branch = "master"
features = ["wgpu", "image", "advanced", "svg", "canvas", "hot", "debug", "lazy", "tokio"]
[dependencies.iced_video_player]
git = "https://github.com/jackpot51/iced_video_player.git"
branch = "cosmic"
features = ["wgpu"]
git = "https://git.tfcconnection.org/chris/iced_video_player"
branch = "master"
# branch = "cosmic"
# [profile.dev]
# opt-level = 3

View file

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

View file

@ -3,8 +3,8 @@ use std::cmp::Ordering;
use std::ops::Deref;
use std::sync::Arc;
use cosmic::iced::clipboard::mime::{AllowedMimeTypes, AsMimeTypes};
use crisp::types::{Keyword, Symbol, Value};
// use cosmic::iced::clipboard::mime::{AllowedMimeTypes, AsMimeTypes};
use miette::Result;
use tracing::{debug, error};
@ -56,29 +56,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 {

View file

@ -1,4 +1,4 @@
// use cosmic::dialog::ashpd::url::Url;
// use iced::dialog::ashpd::url::Url;
use crisp::types::{Keyword, Symbol, Value};
use iced_video_player::Video;
use miette::{miette, Result};

View file

@ -1,6 +1,5 @@
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,7 +7,6 @@ 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 cosmic::iced::Length;
use iced::Length;
struct DoubleSlider<'a, T, Message> {
range: RangeInclusive<T>,

View file

@ -1,30 +1,30 @@
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},
use iced::{
advanced::widget::{tree::State, Widget},
alignment::Vertical,
futures::FutureExt,
theme,
widget::{
button, container, horizontal_space, icon, mouse_area,
button, column, container, horizontal_space, mouse_area,
responsive, row, scrollable, text, text_input, Container,
DndSource, Space, Widget,
Space,
},
Element, Task,
Background, Border, Color, Element, Length, 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},
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,
};
#[derive(Debug, Clone)]
@ -279,40 +279,40 @@ impl<'a> Library {
where
T: Content,
{
let mut row = row::<Message>().spacing(5);
let mut row = row![].spacing(5);
match &model.kind {
LibraryKind::Song => {
row = row
.push(icon::from_name("folder-music-symbolic"));
row = row
.push(textm!("Songs").align_y(Vertical::Center));
row =
row.push(text("Songs").align_y(Vertical::Center));
}
LibraryKind::Video => {
row = row
.push(icon::from_name("folder-videos-symbolic"));
row = row
.push(textm!("Videos").align_y(Vertical::Center));
.push(text("Videos").align_y(Vertical::Center));
}
LibraryKind::Image => {
row = row.push(icon::from_name(
"folder-pictures-symbolic",
));
row = row
.push(textm!("Images").align_y(Vertical::Center));
.push(text("Images").align_y(Vertical::Center));
}
LibraryKind::Presentation => {
row = row.push(icon::from_name(
"x-office-presentation-symbolic",
));
row = row.push(
textm!("Presentations").align_y(Vertical::Center),
text("Presentations").align_y(Vertical::Center),
);
}
};
let item_count = model.items.len();
row = row.push(horizontal_space());
row = row
.push(textm!("{}", item_count).align_y(Vertical::Center));
.push(text!("{}", item_count).align_y(Vertical::Center));
row = row.push(
icon::from_name({
if self.library_open == Some(model.kind) {
@ -332,19 +332,26 @@ impl<'a> Library {
match self.library_hovered {
Some(lib) => Background::Color(
if lib == model.kind {
t.cosmic().button.hover.into()
t.extended_palette()
.primary
.strong
.color
} else {
t.cosmic().button.base.into()
t.extended_palette()
.background
.base
.color
},
),
None => Background::Color(
t.cosmic().button.base.into(),
t.extended_palette()
.background
.base
.color,
),
}
})
.border(Border::default().rounded(
t.cosmic().corner_radii.radius_s,
))
.border(Border::default().rounded(5))
})
.center_x(Length::Fill)
.center_y(Length::Shrink);
@ -367,65 +374,34 @@ impl<'a> Library {
let visual_item = self
.single_item(index, item, model)
.map(|_| Message::None);
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()
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()
},
)
})
.spacing(2)
.width(Length::Fill),
.spacing(2)
.width(Length::Fill),
)
.spacing(5)
.height(Length::Fill);
.spacing(5)
.height(Length::Fill);
let library_toolbar = rowm!(
let library_toolbar = row!(
text_input("Search...", ""),
button::icon(icon::from_name("add"))
button(icon::from_name("add"))
);
let library_column =
column![library_toolbar, items].spacing(3);
@ -446,36 +422,36 @@ impl<'a> Library {
where
T: Content,
{
let text = Container::new(responsive(|size| {
text::heading(elide_text(item.title(), size.width))
let item_text = Container::new(responsive(|size| {
text(elide_text(item.title(), size.width))
.center()
.wrapping(textm::Wrapping::None)
.wrapping(text::Wrapping::None)
.into()
}))
.center_y(20)
.center_x(Length::Fill);
let subtext = container(responsive(|size| {
let color: Color = if item.background().is_some() {
theme::active().cosmic().accent_text_color().into()
} else {
theme::active()
.cosmic()
.destructive_text_color()
if item.background().is_some() {
text(elide_text(item.subtext(), size.width))
.style(text::primary)
.center()
.wrapping(text::Wrapping::None)
.into()
};
text::body(elide_text(item.subtext(), size.width))
.center()
.wrapping(textm::Wrapping::None)
.class(color)
.into()
} else {
text(elide_text(item.subtext(), size.width))
.style(text::primary)
.center()
.wrapping(text::Wrapping::None)
.into()
}
}))
.center_y(20)
.center_x(Length::Fill);
let texts = column([text.into(), subtext.into()]);
let texts = column([item_text.into(), subtext.into()]);
Container::new(
rowm![horizontal_space().width(0), texts]
row![horizontal_space().width(0), texts]
.spacing(10)
.align_y(Vertical::Center),
)
@ -490,9 +466,9 @@ impl<'a> Library {
if model.kind == library
&& selected == index as i32
{
t.cosmic().accent.selected.into()
t.extended_palette().primary.strong.color
} else {
t.cosmic().button.base.into()
t.extended_palette().primary.base.color
}
} else if let Some((library, hovered)) =
self.hovered_item
@ -500,18 +476,15 @@ impl<'a> Library {
if model.kind == library
&& hovered == index as i32
{
t.cosmic().button.hover.into()
t.extended_palette().primary.strong.color
} else {
t.cosmic().button.base.into()
t.extended_palette().primary.base.color
}
} else {
t.cosmic().button.base.into()
t.extended_palette().background.strong.color
},
))
.border(
Border::default()
.rounded(t.cosmic().corner_radii.radius_m),
)
.border(Border::default().rounded(10))
})
.into()
}

View file

@ -1,29 +1,22 @@
use miette::{IntoDiagnostic, Result};
use std::{fs::File, io::BufReader, path::PathBuf, sync::Arc};
use cosmic::{
iced::{
alignment::Horizontal,
border,
font::{Family, Stretch, Style, Weight},
Background, Border, Color, ContentFit, Font, Length, Shadow,
Vector,
},
iced_widget::{
rich_text,
scrollable::{
scroll_to, AbsoluteOffset, Direction, Scrollbar,
},
span, stack, vertical_rule,
},
prelude::*,
use iced::{
alignment::Horizontal,
border,
font::{Family, Stretch, Style, Weight},
widget::{
container, image, mouse_area, responsive, scrollable, text,
Column, Container, Id, Row, Space,
container, image, mouse_area, responsive, rich_text,
scrollable::{
self, scroll_to, AbsoluteOffset, Direction, Id, Scrollbar,
},
span, stack, text, vertical_rule, Column, Container, Row,
Space,
},
Task,
Background, Border, Color, ContentFit, Element, Font, Length,
Shadow, Task, Vector,
};
use iced_video_player::{gst_pbutils, Position, Video, VideoPlayer};
use iced_video_player::{Position, Video, VideoPlayer};
use rodio::{Decoder, OutputStream, Sink};
use tracing::{debug, error, info, warn};
use url::Url;
@ -168,7 +161,7 @@ impl Presenter {
)
},
scroll_id: Id::unique(),
current_font: cosmic::font::default(),
current_font: iced::font::Font::DEFAULT,
}
}
@ -344,27 +337,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
@ -449,7 +442,7 @@ impl Presenter {
.style(move |t| {
let mut style =
container::Style::default();
let theme = t.cosmic();
let theme = t;
let hovered = self.hovered_slide
== Some((
item_index,
@ -459,19 +452,25 @@ impl Presenter {
Some(Background::Color(
if is_current_slide {
theme
.accent
.base
.into()
.extended_palette(
)
.secondary
.strong
.color
} else if hovered {
theme
.accent
.hover
.into()
.extended_palette(
)
.secondary
.strong
.color
} else {
theme
.palette
.neutral_3
.into()
.extended_palette(
)
.background
.neutral
.color
},
));
style.border = Border::default()
@ -504,7 +503,7 @@ impl Presenter {
.padding(10),
)
.interaction(
cosmic::iced::mouse::Interaction::Pointer,
iced::mouse::Interaction::Pointer,
)
.on_move(move |_| {
Message::HoveredSlide(Some((
@ -522,11 +521,11 @@ impl Presenter {
let row = Row::from_vec(slides)
.spacing(10)
.padding([20, 15]);
let label = text::body(item.title.clone());
let label = text(item.title.clone());
let label_container = container(label)
.align_top(Length::Fill)
.align_left(Length::Fill)
.padding([0, 0, 0, 35]);
.padding([0, 35]);
let divider = vertical_rule(2);
items.push(
container(stack!(row, label_container))
@ -536,15 +535,16 @@ impl Presenter {
items.push(divider.into());
},
);
let row =
scrollable(container(Row::from_vec(items)).style(|t| {
let row = scrollable::Scrollable::new(
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 +578,7 @@ impl Presenter {
// Container::new(container)
// .style(move |t| {
// let mut style = container::Style::default();
// let theme = t.cosmic();
// let theme = t.iced();
// let hovered = self.hovered_slide == slide_id;
// style.background = Some(Background::Color(
// if is_current_slide {
@ -617,7 +617,7 @@ impl Presenter {
// .height(100)
// .padding(10),
// )
// .interaction(cosmic::iced::mouse::Interaction::Pointer)
// .interaction(iced::iced::mouse::Interaction::Pointer)
// .on_move(move |_| Message::HoveredSlide(slide_id))
// .on_exit(Message::HoveredSlide(-1))
// .on_press(Message::SlideChange(slide.clone()));
@ -709,7 +709,13 @@ pub(crate) fn slide_view(
let lines = slide_text.lines();
let text: Vec<Element<Message>> = lines
.map(|t| {
rich_text([span(format!("{}\n", t))
rich_text::<
'_,
&str,
Message,
iced::Theme,
iced::Renderer,
>([span(format!("{}\n", t))
.background(
Background::Color(Color::BLACK)
.scale_alpha(0.4),
@ -823,15 +829,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())
})

View file

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

View file

@ -1,27 +1,25 @@
use std::{io, path::PathBuf};
use cosmic::{
dialog::file_chooser::open::Dialog,
iced::{
font::{Family, Stretch, Style, Weight},
Font, Length,
},
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::{
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,
},
Element, Font, Length, Task,
};
use iced_video_player::Video;
use tracing::{debug, error};
use crate::{
core::{service_items::ServiceTrait, songs::Song},
ui::slide_editor::{self, SlideEditor},
ui::{
slide_editor::{self, SlideEditor},
widgets::icon,
},
Background, BackgroundKind,
};
@ -133,7 +131,7 @@ impl SongEditor {
audio: PathBuf::new(),
background: None,
video: None,
current_font: cosmic::font::default(),
current_font: iced::font::Font::DEFAULT,
ccli: "8".to_owned(),
slide_state: SlideEditor::default(),
}
@ -273,100 +271,93 @@ impl SongEditor {
let slide_preview = container(self.slide_preview())
.width(Length::FillPortion(2));
let column = column::with_children(vec![
let column = column![
self.toolbar(),
row![
container(self.left_column())
.center_x(Length::FillPortion(2)),
container(slide_preview)
.center_x(Length::FillPortion(3))
]
.into(),
])
.spacing(theme::active().cosmic().space_l());
],
]
.spacing(15);
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::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()
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()
}
fn left_column(&self) -> Element<Message> {
let title_input = text_input("song", &self.title)
.on_input(Message::ChangeTitle)
.label("Song Title");
let title_input = text_input("song", self.title.as_ref())
.on_input(|_| Message::ChangeTitle);
let author_input = text_input("author", &self.author)
.on_input(Message::ChangeAuthor)
.label("Song Author");
.on_input(|_| Message::ChangeAuthor);
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::with_children(vec![
let lyric_input = column![
lyric_title.into(),
text_editor(&self.lyrics)
.on_action(Message::ChangeLyrics)
.height(Length::Fill)
.into(),
])
]
.spacing(5);
column::with_children(vec![
column![
title_input.into(),
author_input.into(),
verse_input.into(),
lyric_input.into(),
])
]
.spacing(25)
.width(Length::FillPortion(2))
.into()
@ -401,16 +392,20 @@ order",
)
},
)
.width(theme::active().cosmic().space_xxl());
.width(200);
let background_selector = button::icon(
let background_selector = button(row!(
icon::from_name("folder-pictures-symbolic").scale(2),
)
.label("Background")
.tooltip("Select an image or video background")
"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,
@ -452,18 +447,19 @@ impl Default for SongEditor {
}
async fn pick_background() -> Result<PathBuf, SongError> {
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...")
// let dialog =
// AsyncFileDialog::new().set_title("Choose a background...");
// dialog
// .pick_file()
// .await
// .ok_or(SongError::DialogClosed)
// .map(|file| file.path().to_owned())
// .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())
}
#[derive(Debug, Clone)]

View file

@ -4,13 +4,10 @@ use std::{
};
use colors_transform::Rgb;
use cosmic::{
iced::{
font::{Style, Weight},
Length, Size,
},
prelude::*,
use iced::{
font::{Style, Weight},
widget::{container, svg::Handle, Svg},
Element, Length, Size,
};
use tracing::error;
@ -46,13 +43,11 @@ pub struct Font {
size: u8,
}
impl From<cosmic::font::Font> for Font {
fn from(value: cosmic::font::Font) -> Self {
impl From<iced::font::Font> for Font {
fn from(value: iced::font::Font) -> Self {
Self {
name: match value.family {
cosmic::iced::font::Family::Name(name) => {
name.to_string()
}
iced::font::Family::Name(name) => name.to_string(),
_ => "Quicksand Bold".into(),
},
size: 20,

View file

@ -0,0 +1,115 @@
// 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)),
}
}

187
src/ui/widgets/icon/mod.rs Normal file
View file

@ -0,0 +1,187 @@
// 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

@ -0,0 +1,165 @@
// 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 +1,2 @@
// 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 = cosmic::iced::wgpu::util::initialize_adapter_from_env_or_default(&instance, Some(&surface))
let adapter = iced::iced::wgpu::util::initialize_adapter_from_env_or_default(&instance, Some(&surface))
.await
.expect("Failed to find an appropriate adapter");
let (device, queue) = adapter