Compare commits

...

2 commits

Author SHA1 Message Date
bb1057e950 starting to add my own service widget
Some checks are pending
/ test (push) Waiting to run
2025-09-22 08:28:48 -05:00
74ae0e8a17 fixing up the presenter and editor more 2025-09-22 08:28:29 -05:00
6 changed files with 500 additions and 35 deletions

View file

@ -9,26 +9,30 @@ use cosmic::app::{Core, Settings, Task};
use cosmic::iced::alignment::Vertical;
use cosmic::iced::keyboard::{Key, Modifiers};
use cosmic::iced::window::{Mode, Position};
use cosmic::iced::{self, event, window, Length, Point};
use cosmic::iced::{self, event, window, Color, Length, Point};
use cosmic::iced_futures::Subscription;
use cosmic::iced_runtime::dnd::DndAction;
use cosmic::iced_widget::{column, row, stack};
use cosmic::theme;
use cosmic::widget::dnd_destination::dnd_destination;
use cosmic::widget::menu::key_bind::Modifier;
use cosmic::widget::menu::{ItemWidth, KeyBind};
use cosmic::widget::nav_bar::nav_bar_style;
use cosmic::widget::tooltip::Position as TPosition;
use cosmic::widget::Container;
use cosmic::widget::{
button, horizontal_space, mouse_area, nav_bar, search_input,
tooltip, vertical_space, Space,
button, dnd_source, horizontal_space, mouse_area, nav_bar,
search_input, tooltip, vertical_space, RcElementWrapper, Space,
};
use cosmic::widget::{container, text};
use cosmic::widget::{icon, slider};
use cosmic::widget::{menu, Container};
use cosmic::{executor, Application, ApplicationExt, Element};
use crisp::types::Value;
use lisp::parse_lisp;
use miette::{miette, Result};
use rayon::prelude::*;
use resvg::usvg::fontdb;
use std::collections::HashMap;
use std::fs::read_to_string;
use std::path::PathBuf;
use std::sync::Arc;
@ -119,6 +123,7 @@ struct App {
search_id: cosmic::widget::Id,
library_dragged_item: Option<ServiceItem>,
fontdb: Arc<fontdb::Database>,
menu_keys: HashMap<KeyBind, MenuAction>,
}
#[derive(Debug, Clone)]
@ -147,6 +152,35 @@ enum Message {
CloseSearch,
UpdateSearchResults(Vec<ServiceItem>),
OpenEditor(ServiceItem),
New,
Open,
OpenFile(PathBuf),
Save(Option<PathBuf>),
SaveAs,
OpenSettings,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum MenuAction {
New,
Save,
SaveAs,
Open,
OpenSettings,
}
impl menu::Action for MenuAction {
type Message = Message;
fn message(&self) -> Self::Message {
match self {
MenuAction::New => Message::New,
MenuAction::Save => Message::Save(None),
MenuAction::SaveAs => Message::SaveAs,
MenuAction::Open => Message::Open,
MenuAction::OpenSettings => Message::OpenSettings,
}
}
}
const HEADER_SPACE: u16 = 6;
@ -230,6 +264,28 @@ impl cosmic::Application for App {
// nav_model.insert().text(item.title()).data(item.clone());
// }
let mut menu_keys = HashMap::new();
menu_keys.insert(
KeyBind {
modifiers: vec![Modifier::Ctrl],
key: Key::Character("s".into()),
},
MenuAction::Save,
);
menu_keys.insert(
KeyBind {
modifiers: vec![Modifier::Ctrl],
key: Key::Character("o".into()),
},
MenuAction::Open,
);
menu_keys.insert(
KeyBind {
modifiers: vec![Modifier::Ctrl],
key: Key::Character(".".into()),
},
MenuAction::OpenSettings,
);
// nav_model.activate_position(0);
let mut app = Self {
presenter,
@ -251,6 +307,7 @@ impl cosmic::Application for App {
current_item: (0, 0),
library_dragged_item: None,
fontdb: Arc::clone(&fontdb),
menu_keys,
};
let mut batch = vec![];
@ -270,7 +327,46 @@ impl cosmic::Application for App {
}
fn header_start(&self) -> Vec<Element<Self::Message>> {
vec![]
let file_menu = menu::Tree::with_children(
Into::<Element<Message>>::into(menu::root("File")),
menu::items(
&self.menu_keys,
vec![
menu::Item::Button("New", None, MenuAction::New),
menu::Item::Button(
"Open",
None,
MenuAction::Open,
),
menu::Item::Button(
"Save",
None,
MenuAction::Save,
),
menu::Item::Button(
"Save As",
None,
MenuAction::SaveAs,
),
],
),
);
let settings_menu = menu::Tree::with_children(
Into::<Element<Message>>::into(menu::root("Settings")),
menu::items(
&self.menu_keys,
vec![menu::Item::Button(
"Open Settings",
None,
MenuAction::OpenSettings,
)],
),
);
let menu_bar =
menu::bar::<Message>(vec![file_menu, settings_menu])
.item_width(ItemWidth::Static(250))
.main_offset(10);
vec![menu_bar.into()]
}
fn header_center(&self) -> Vec<Element<Self::Message>> {
@ -978,6 +1074,34 @@ impl cosmic::Application for App {
ServiceItemKind::Content(_slide) => todo!(),
}
}
Message::New => {
debug!("new file");
Task::none()
}
Message::Open => {
debug!("Open file");
Task::none()
}
Message::OpenFile(file) => {
debug!(?file, "opening file");
Task::none()
}
Message::Save(file) => {
let Some(file) = file else {
debug!("saving current");
return Task::none();
};
debug!(?file, "saving new file");
Task::none()
}
Message::SaveAs => {
debug!("saving as a file");
Task::none()
}
Message::OpenSettings => {
debug!("Opening settings");
Task::none()
}
}
}
@ -1237,6 +1361,15 @@ where
}
}
match (key, modifiers) {
(Key::Character(k), Modifiers::CTRL) if k == *"s" => {
self.update(Message::Save(None))
}
(Key::Character(k), Modifiers::CTRL) if k == *"o" => {
self.update(Message::Open)
}
(Key::Character(k), Modifiers::CTRL) if k == *"." => {
self.update(Message::OpenSettings)
}
(Key::Character(k), Modifiers::CTRL)
if k == *"k" || k == *"f" =>
{
@ -1306,8 +1439,7 @@ where
// .icon_size(cosmic::theme::spacing().space_l)
.class(cosmic::theme::style::Button::HeaderBar)
// .spacing(cosmic::theme::spacing().space_l)
// .padding(cosmic::theme::spacing().space_m)
// .height(cosmic::theme::spacing().space_xxxl)
.height(cosmic::theme::spacing().space_xl)
.width(Length::Fill)
.on_press(Message::ChangeServiceItem(index));
let tooltip = tooltip(button,

View file

@ -3,6 +3,7 @@ use crate::core::model::LibraryKind;
pub mod double_ended_slider;
pub mod library;
pub mod presenter;
pub mod service;
pub mod slide_editor;
pub mod song_editor;
pub mod text_svg;

View file

@ -399,21 +399,11 @@ impl Presenter {
}
pub fn view(&self) -> Element<Message> {
slide_view(
self.current_slide.clone(),
&self.video,
false,
true,
)
slide_view(&self.current_slide, &self.video, false, true)
}
pub fn view_preview(&self) -> Element<Message> {
slide_view(
self.current_slide.clone(),
&self.video,
false,
false,
)
slide_view(&self.current_slide, &self.video, false, false)
}
pub fn preview_bar(&self) -> Element<Message> {
@ -423,19 +413,6 @@ impl Presenter {
let mut slides = vec![];
item.slides.iter().enumerate().for_each(
|(slide_index, slide)| {
let font_name = slide.font().into_boxed_str();
let family =
Family::Name(Box::leak(font_name));
let weight = Weight::Normal;
let stretch = Stretch::Normal;
let style = Style::Normal;
let font = Font {
family,
weight,
stretch,
style,
};
let is_current_slide =
(item_index, slide_index)
== (
@ -444,7 +421,7 @@ impl Presenter {
);
let container = slide_view(
slide.clone(),
&slide,
&self.video,
true,
false,
@ -705,7 +682,7 @@ fn scale_font(font_size: f32, width: f32) -> f32 {
}
pub(crate) fn slide_view<'a>(
slide: Slide,
slide: &'a Slide,
video: &'a Option<Video>,
delegate: bool,
hide_mouse: bool,

351
src/ui/service.rs Normal file
View file

@ -0,0 +1,351 @@
use std::any::Any;
use cosmic::iced::{self, Size};
use cosmic::iced_core::window;
use cosmic::{
iced::{
clipboard::dnd::{DndAction, DndEvent, SourceEvent},
event, mouse, overlay, Event, Length, Point, Rectangle,
Vector,
},
iced_core::{
self, layout, renderer,
widget::{tree, Tree},
Clipboard, Shell,
},
widget::{container, Id, Widget},
Element,
};
use crate::core::service_items::ServiceItem;
pub fn service<'a, Message: Clone + 'static>(
service: &'a Vec<ServiceItem>,
) -> Service<'a, Message> {
Service::new(service)
}
pub struct Service<'a, Message> {
service: &'a Vec<ServiceItem>,
on_start: Option<Message>,
on_cancelled: Option<Message>,
on_finish: Option<Message>,
drag_threshold: f32,
width: Length,
height: Length,
}
impl<'a, Message: Clone + 'static> Service<'a, Message> {
pub fn new(service: &'a Vec<ServiceItem>) -> Self {
Self {
service,
drag_threshold: 8.0,
on_start: None,
on_cancelled: None,
on_finish: None,
width: Length::Fill,
height: Length::Fill,
}
}
#[must_use]
pub fn drag_threshold(mut self, threshold: f32) -> Self {
self.drag_threshold = threshold;
self
}
// pub fn start_dnd(
// &self,
// clipboard: &mut dyn Clipboard,
// bounds: Rectangle,
// offset: Vector,
// ) {
// let Some(content) = self.drag_content.as_ref().map(|f| f())
// else {
// return;
// };
// iced_core::clipboard::start_dnd(
// clipboard,
// false,
// if let Some(window) = self.window.as_ref() {
// Some(iced_core::clipboard::DndSource::Surface(
// *window,
// ))
// } else {
// Some(iced_core::clipboard::DndSource::Widget(
// self.id.clone(),
// ))
// },
// self.drag_icon.as_ref().map(|f| {
// let (icon, state, offset) = f(offset);
// iced_core::clipboard::IconSurface::new(
// container(icon)
// .width(Length::Fixed(bounds.width))
// .height(Length::Fixed(bounds.height))
// .into(),
// state,
// offset,
// )
// }),
// Box::new(content),
// DndAction::Move,
// );
// }
pub fn on_start(mut self, on_start: Option<Message>) -> Self {
self.on_start = on_start;
self
}
pub fn on_cancel(
mut self,
on_cancelled: Option<Message>,
) -> Self {
self.on_cancelled = on_cancelled;
self
}
pub fn on_finish(mut self, on_finish: Option<Message>) -> Self {
self.on_finish = on_finish;
self
}
}
impl<Message: Clone + 'static>
Widget<Message, cosmic::Theme, cosmic::Renderer>
for Service<'_, Message>
{
fn size(&self) -> iced_core::Size<Length> {
Size {
width: Length::Fill,
height: Length::Fill,
}
}
fn layout(
&self,
_tree: &mut Tree,
_renderer: &cosmic::Renderer,
limits: &layout::Limits,
) -> layout::Node {
layout::atomic(limits, self.width, self.height)
}
// fn operate(
// &self,
// tree: &mut Tree,
// layout: layout::Layout<'_>,
// renderer: &cosmic::Renderer,
// operation: &mut dyn iced_core::widget::Operation<()>,
// ) {
// operation.custom(
// (&mut tree.state) as &mut dyn Any,
// Some(&self.id),
// );
// operation.container(
// Some(&self.id),
// layout.bounds(),
// &mut |operation| {
// self.container.as_widget().operate(
// &mut tree.children[0],
// layout,
// renderer,
// operation,
// )
// },
// );
// }
fn on_event(
&mut self,
tree: &mut Tree,
event: Event,
layout: layout::Layout<'_>,
cursor: mouse::Cursor,
renderer: &cosmic::Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) -> event::Status {
let state = tree.state.downcast_mut::<State>();
match event {
Event::Mouse(mouse_event) => match mouse_event {
mouse::Event::ButtonPressed(mouse::Button::Left) => {
if let Some(position) = cursor.position() {
if !state.hovered {
return event::Status::Ignored;
}
state.left_pressed_position = Some(position);
return event::Status::Captured;
}
}
mouse::Event::ButtonReleased(mouse::Button::Left)
if state.left_pressed_position.is_some() =>
{
state.left_pressed_position = None;
return event::Status::Captured;
}
mouse::Event::CursorMoved { .. } => {
if let Some(position) = cursor.position() {
if state.hovered {
// We ignore motion if we do not possess drag content by now.
if let Some(left_pressed_position) =
state.left_pressed_position
{
if position
.distance(left_pressed_position)
> self.drag_threshold
{
if let Some(on_start) =
self.on_start.as_ref()
{
shell
.publish(on_start.clone())
}
let offset = Vector::new(
left_pressed_position.x
- layout.bounds().x,
left_pressed_position.y
- layout.bounds().y,
);
state.is_dragging = true;
state.left_pressed_position =
None;
}
}
if !cursor.is_over(layout.bounds()) {
state.hovered = false;
return event::Status::Ignored;
}
} else if cursor.is_over(layout.bounds()) {
state.hovered = true;
}
return event::Status::Captured;
}
}
_ => return event::Status::Ignored,
},
Event::Dnd(DndEvent::Source(SourceEvent::Cancelled)) => {
if state.is_dragging {
if let Some(m) = self.on_cancelled.as_ref() {
shell.publish(m.clone());
}
state.is_dragging = false;
return event::Status::Captured;
}
return event::Status::Ignored;
}
Event::Dnd(DndEvent::Source(SourceEvent::Finished)) => {
if state.is_dragging {
if let Some(m) = self.on_finish.as_ref() {
shell.publish(m.clone());
}
state.is_dragging = false;
return event::Status::Captured;
}
return event::Status::Ignored;
}
_ => return event::Status::Ignored,
}
event::Status::Ignored
}
// fn mouse_interaction(
// &self,
// tree: &Tree,
// layout: layout::Layout<'_>,
// cursor_position: mouse::Cursor,
// viewport: &Rectangle,
// renderer: &cosmic::Renderer,
// ) -> mouse::Interaction {
// let state = tree.state.downcast_ref::<State>();
// if state.is_dragging {
// return mouse::Interaction::Grabbing;
// }
// self.container.as_widget().mouse_interaction(
// &tree.children[0],
// layout,
// cursor_position,
// viewport,
// renderer,
// )
// }
fn draw(
&self,
tree: &Tree,
renderer: &mut cosmic::Renderer,
theme: &cosmic::Theme,
renderer_style: &renderer::Style,
layout: layout::Layout<'_>,
cursor_position: mouse::Cursor,
viewport: &Rectangle,
) {
let state = tree.state.downcast_mut::<State>();
for item in self.service {
todo!()
}
}
// fn overlay<'b>(
// &'b mut self,
// tree: &'b mut Tree,
// layout: layout::Layout<'_>,
// renderer: &cosmic::Renderer,
// translation: Vector,
// ) -> Option<
// overlay::Element<
// 'b,
// Message,
// cosmic::Theme,
// cosmic::Renderer,
// >,
// > {
// self.container.as_widget_mut().overlay(
// &mut tree.children[0],
// layout,
// renderer,
// translation,
// )
// }
// #[cfg(feature = "a11y")]
// /// get the a11y nodes for the widget
// fn a11y_nodes(
// &self,
// layout: iced_core::Layout<'_>,
// state: &Tree,
// p: mouse::Cursor,
// ) -> iced_accessibility::A11yTree {
// let c_state = &state.children[0];
// self.container.as_widget().a11y_nodes(layout, c_state, p)
// }
}
impl<'a, Message: Clone + 'static> From<Service<'a, Message>>
for Element<'a, Message>
{
fn from(e: Service<'a, Message>) -> Element<'a, Message> {
Element::new(e)
}
}
/// Local state of the [`MouseListener`].
#[derive(Debug, Default)]
struct State {
hovered: bool,
left_pressed_position: Option<Point>,
is_dragging: bool,
cached_bounds: Rectangle,
}
impl State {
fn new() -> Self {
Self::default()
}
}

View file

@ -314,7 +314,7 @@ impl SongEditor {
.map(|(index, slide)| {
container(
slide_view(
slide.clone(),
&slide,
if index == 0 {
&self.video
} else {

View file

@ -1,6 +1,9 @@
#+TITLE: The Task list for Lumina
* TODO [#A] Add removal and reordering of service_items
* TODO Add OBS integration
This will be based on each slide having the ability to activate an OBS scene when it is active.
* TODO Move text_generation function to be asynchronous so that UI doesn't lock up during song editing.
* TODO Build a video editor
* TODO Build an image editor
@ -73,6 +76,7 @@ This needs lots more attention
This will make it so that we can add styling to the text like borders and backgrounds or highlights. Maybe in the future it'll add shadows too.
* DONE Check into =mupdf-rs= for loading PDF's.
* DONE Build Menu
* DONE Find a way for text to pass through a service item to a slide i.e. content piece
This proved easier by just creating the =Slide= first and inserting it into the =ServiceItem=.
* DONE [#A] Change return type of all components to an Action enum instead of the Task<Message> type [0%] [0/0]