diff --git a/Cargo.lock b/Cargo.lock index ca69fde..efdad78 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -103,6 +103,22 @@ dependencies = [ "libc", ] +[[package]] +name = "animations" +version = "0.1.0" +dependencies = [ + "anyhow", + "lazy_static", + "mctk_core", + "mctk_macros", + "mctk_smithay", + "rand", + "smithay-client-toolkit 0.18.1", + "tokio", + "tracing", + "tracing-subscriber", +] + [[package]] name = "anyhow" version = "1.0.80" diff --git a/core/src/animations.rs b/core/src/animations.rs new file mode 100644 index 0000000..c8535a6 --- /dev/null +++ b/core/src/animations.rs @@ -0,0 +1,121 @@ +use std::hash::Hash; + +pub trait EasingFunction { + fn ease(t: f64) -> f64; +} + +#[derive(Copy, Clone, Debug)] +pub enum AnimationRepeat { + Once, + Loop, + PingPong, +} + +#[derive(Copy, Clone, Debug)] +pub struct Animation { + start_time: std::time::Instant, + duration: std::time::Duration, + repeat: AnimationRepeat, + _phantom: std::marker::PhantomData, +} + +impl Default for Animation { + fn default() -> Self { + Self::new(std::time::Duration::from_secs(1), AnimationRepeat::Once) + } +} + +impl Hash for Animation { + fn hash(&self, state: &mut H) { + self.start_time.hash(state); + self.duration.hash(state); + (((u32::MAX as f64) * self.get_value()) as u32).hash(state); + } +} + +impl Animation { + pub fn new(duration: std::time::Duration, repeat: AnimationRepeat) -> Self { + Self { + start_time: std::time::Instant::now(), + duration, + repeat, + _phantom: std::marker::PhantomData, + } + } + + pub fn reset(&mut self) { + self.start_time = std::time::Instant::now(); + } + + pub fn get_value(&self) -> f64 { + let t = self.start_time.elapsed().as_secs_f64() / self.duration.as_secs_f64(); + match self.repeat { + AnimationRepeat::Once => { + if t >= 1.0 { + 1.0 + } else { + F::ease(t) + } + } + AnimationRepeat::Loop => F::ease(t % 1.0), + AnimationRepeat::PingPong => { + let t = t % 2.0; + if t >= 1.0 { + F::ease(2.0 - t) + } else { + F::ease(t) + } + } + } + } +} + +pub mod easing_functions { + use super::EasingFunction; + + pub struct Linear; + impl EasingFunction for Linear { + fn ease(t: f64) -> f64 { + t + } + } + + pub struct Quadratic; + impl EasingFunction for Quadratic { + fn ease(t: f64) -> f64 { + t * t + } + } + + pub struct Cubic; + impl EasingFunction for Cubic { + fn ease(t: f64) -> f64 { + t * t * t + } + } + + pub struct EaseOutQuadratic; + impl EasingFunction for EaseOutQuadratic { + fn ease(t: f64) -> f64 { + -t * (t - 2.0) + } + } + + pub struct EaseInQuadratic; + impl EasingFunction for EaseInQuadratic { + fn ease(t: f64) -> f64 { + t * t + } + } + + pub struct EaseInOutQuadratic; + impl EasingFunction for EaseInOutQuadratic { + fn ease(t: f64) -> f64 { + if t < 0.5 { + 2.0 * t * t + } else { + -1.0 + (4.0 - 2.0 * t) * t + } + } + } +} diff --git a/core/src/lib.rs b/core/src/lib.rs index e5d969d..cf69f32 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -24,6 +24,9 @@ pub mod reexports { #[macro_use] pub mod widgets; +#[macro_use] +pub mod animations; + pub mod types; pub use types::*; diff --git a/examples/animations/Cargo.toml b/examples/animations/Cargo.toml new file mode 100644 index 0000000..20d25a2 --- /dev/null +++ b/examples/animations/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "animations" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +mctk_core = { path = "../../core", version = "0.1.0" } +mctk_smithay = { path = "../../backends/smithay", version = "0.1.0" } +mctk_macros = { path = "../../macros", version = "0.1.0" } +smithay-client-toolkit = "0.18.0" +tracing = "0.1.37" +tracing-subscriber = { version = "0.3.17", features = ["env-filter"]} +anyhow = { version = "1.0.75", features = ["backtrace"]} +tokio = { version = "1.33", features = ["full"] } +lazy_static = "1.5.0" +rand = "0.8.5" diff --git a/examples/animations/src/assets/fonts/SpaceGrotesk-Regular.ttf b/examples/animations/src/assets/fonts/SpaceGrotesk-Regular.ttf new file mode 100644 index 0000000..981bcf5 Binary files /dev/null and b/examples/animations/src/assets/fonts/SpaceGrotesk-Regular.ttf differ diff --git a/examples/animations/src/assets/icons/eye.svg b/examples/animations/src/assets/icons/eye.svg new file mode 100644 index 0000000..572f26c --- /dev/null +++ b/examples/animations/src/assets/icons/eye.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/examples/animations/src/main.rs b/examples/animations/src/main.rs new file mode 100644 index 0000000..6ed5916 --- /dev/null +++ b/examples/animations/src/main.rs @@ -0,0 +1,240 @@ +use mctk_core::animations::{easing_functions::*, Animation}; +use mctk_core::context::Model; +use mctk_core::layout::Alignment; +use mctk_core::prelude::*; +use mctk_core::reexports::smithay_client_toolkit::{ + reexports::calloop::{self, channel::Event}, + shell::wlr_layer, +}; +use mctk_core::widgets::Text; +use mctk_smithay::layer_shell::layer_surface::LayerOptions; +use mctk_smithay::layer_shell::layer_window::{LayerWindow, LayerWindowParams}; +use mctk_smithay::xdg_shell::xdg_window::{self, XdgWindowMessage, XdgWindowParams}; +use mctk_smithay::{WindowInfo, WindowMessage, WindowOptions}; +use smithay_client_toolkit::reexports::calloop::channel::Sender; +use std::any::Any; +use std::collections::HashMap; +use std::fmt::Debug; +use std::hash::Hash; +use tracing_subscriber::fmt::format; + +#[derive(Debug)] +pub enum AppMessage { + Exit, +} + +#[derive(Clone)] +pub struct AppParams { + app_channel: Option>, +} + +#[derive(Default)] +pub struct AppState { + window_sender: Option>, + app_channel: Option>, + linear_animation: Animation, + ease_in_out_animation: Animation, + ease_in_animation: Animation, + ease_out_animation: Animation, +} + +impl Debug for AppState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("AppState").finish() + } +} + +#[component(State = "AppState")] +#[derive(Debug, Default)] +pub struct App {} + +#[state_component_impl(AppState)] +impl Component for App { + fn init(&mut self) { + self.state = Some(AppState { + window_sender: None, + app_channel: None, + linear_animation: Animation::new( + std::time::Duration::from_secs(1), + animations::AnimationRepeat::PingPong, + ), + ease_in_out_animation: Animation::new( + std::time::Duration::from_secs(1), + animations::AnimationRepeat::PingPong, + ), + ease_in_animation: Animation::new( + std::time::Duration::from_secs(1), + animations::AnimationRepeat::PingPong, + ), + ease_out_animation: Animation::new( + std::time::Duration::from_secs(1), + animations::AnimationRepeat::PingPong, + ), + }); + } + + fn render_hash(&self, hasher: &mut ComponentHasher) { + self.state_ref().linear_animation.hash(hasher); + self.state_ref().ease_in_animation.hash(hasher); + self.state_ref().ease_out_animation.hash(hasher); + self.state_ref().ease_in_out_animation.hash(hasher); + } + + fn props_hash(&self, hasher: &mut ComponentHasher) { + self.state_ref().linear_animation.hash(hasher); + self.state_ref().ease_in_animation.hash(hasher); + self.state_ref().ease_out_animation.hash(hasher); + self.state_ref().ease_in_out_animation.hash(hasher); + } + + fn on_tick(&mut self, _event: &mut mctk_core::event::Event) { + self.dirty = true; + } + + fn view(&self) -> Option { + let linear_animation = self.state_ref().linear_animation.get_value(); + let linear_label = node!( + Text::new(txt!(format!("Liner Animation",))) + .style("color", Color::WHITE) + .style("size", 25.0), + lay![size: size!(480.0, 50.0), margin: [10.0, 25.0, 0.0, 0.0],] + ); + let linear_rectangle = node!( + RoundedRect::new(Color::WHITE, 100.0), + lay![ + size: size!(50.0, 50.0), + margin: [25.0, 25.0 + 375.0 * linear_animation , 0.0, 0.0], + ] + ) + .key((linear_animation * 1000000.0) as u64); + + let ease_in_out_label = node!( + Text::new(txt!(format!("Ease In Out",))) + .style("color", Color::WHITE) + .style("size", 25.0), + lay![size: size!(480.0, 50.0), margin: [10.0, 25.0, 0.0, 0.0],] + ); + let ease_in_out_animation = self.state_ref().ease_in_out_animation.get_value(); + let ease_in_out_rectangle = node!( + RoundedRect::new(Color::WHITE, 100.0), + lay![ + size: size!(50.0, 50.0), + margin: [25.0, 25.0 + 375.0 * ease_in_out_animation , 0.0, 0.0], + ] + ) + .key((ease_in_out_animation * 1000000.0) as u64); + + let mut base = node!( + Div::new().bg(Color::BLACK), + lay![direction: Direction::Column, size: size!(480.0, 480.0)] + ); + base = base.push(linear_rectangle); + base = base.push(linear_label); + + base = base.push(ease_in_out_rectangle); + base = base.push(ease_in_out_label); + Some(base) + } + + fn update(&mut self, message: Message) -> Vec { + vec![message] + } +} + +// Layer Surface App +#[tokio::main] +async fn main() { + let id = 1; + let ui_t = std::thread::spawn(move || { + let _ = launch_ui(id); + }); + ui_t.join().unwrap(); +} + +impl RootComponent for App { + fn root(&mut self, w: &dyn std::any::Any, app_params: &dyn Any) { + let app_params = app_params.downcast_ref::().unwrap(); + self.state_mut().app_channel = app_params.app_channel.clone(); + } +} + +fn launch_ui(id: i32) -> anyhow::Result<()> { + let assets: HashMap = HashMap::new(); + let mut svgs: HashMap = HashMap::new(); + + svgs.insert( + "eye_icon".to_string(), + "./src/assets/icons/eye.svg".to_string(), + ); + + let mut fonts = cosmic_text::fontdb::Database::new(); + fonts.load_system_fonts(); + + fonts.load_font_data(include_bytes!("assets/fonts/SpaceGrotesk-Regular.ttf").into()); + + let window_opts = WindowOptions { + height: 480_u32, + width: 480_u32, + scale_factor: 1.0, + }; + + println!("id: {id:?}"); + let window_info = WindowInfo { + id: format!("{:?}{:?}", "mctk.examples.animations".to_string(), id), + title: format!("{:?}{:?}", "mctk.examples.animations".to_string(), id), + namespace: format!("{:?}{:?}", "mctk.examples.animations".to_string(), id), + }; + let layer_shell_opts = LayerOptions { + anchor: wlr_layer::Anchor::LEFT | wlr_layer::Anchor::RIGHT | wlr_layer::Anchor::TOP, + layer: wlr_layer::Layer::Top, + keyboard_interactivity: wlr_layer::KeyboardInteractivity::Exclusive, + namespace: Some(window_info.namespace.clone()), + zone: 0_i32, + }; + + let (app_channel_tx, app_channel_rx) = calloop::channel::channel(); + let (mut app, mut event_loop, window_tx) = LayerWindow::open_blocking::( + LayerWindowParams { + window_info, + window_opts, + fonts, + assets, + layer_shell_opts, + svgs, + ..Default::default() + }, + AppParams { + app_channel: Some(app_channel_tx), + }, + ); + let handle = event_loop.handle(); + let window_tx_2 = window_tx.clone(); + + let window_tx_channel = window_tx.clone(); + + let _ = handle.insert_source(app_channel_rx, move |event: Event, _, app| { + match event { + calloop::channel::Event::Msg(msg) => match msg { + AppMessage::Exit => { + println!("app channel message {:?}", AppMessage::Exit); + let _ = window_tx_2.send(WindowMessage::WindowEvent { + event: mctk_smithay::WindowEvent::CloseRequested, + }); + } + }, + calloop::channel::Event::Closed => { + println!("calloop::event::closed"); + } + }; + }); + + loop { + let _ = event_loop.dispatch(None, &mut app); + + if app.is_exited { + break; + } + } + + Ok(()) +}