diff --git a/mangades-plain/src/anilist/mod.rs b/mangades-plain/src/anilist/mod.rs index 7a5fa98..1e64d7f 100644 --- a/mangades-plain/src/anilist/mod.rs +++ b/mangades-plain/src/anilist/mod.rs @@ -5,74 +5,74 @@ use mangui::nodes::image::ImageLoad; #[derive(Deserialize, Debug)] struct GraphqlResponse { - data: T + pub(crate) data: T } #[derive(Deserialize, Debug)] pub struct MediaListCollectionData { #[serde(rename = "MediaListCollection")] - media_list_collection: MediaListCollection + pub(crate) media_list_collection: MediaListCollection } #[derive(Deserialize, Debug)] pub struct MediaListCollection { - lists: Vec + pub(crate) lists: Vec } #[derive(Deserialize, Debug)] -struct MediaList { - name: String, +pub(crate) struct MediaList { + pub(crate) name: String, #[serde(rename = "isCustomList")] - is_custom_list: bool, - status: String, + pub(crate) is_custom_list: bool, + pub(crate) status: String, #[serde(rename = "isSplitCompletedList")] - is_split_completed_list: bool, - entries: Vec, + pub(crate) is_split_completed_list: bool, + pub(crate) entries: Vec, } #[derive(Deserialize, Debug)] -struct MediaListEntry { - status: String, - progress: i32, +pub(crate) struct MediaListEntry { + pub(crate) status: String, + pub(crate) progress: i32, #[serde(rename = "progressVolumes")] - progress_volumes: i32, - repeat: i32, - priority: i32, - private: bool, - notes: Option, - score: f32, - media: MediaEntry, + pub(crate) progress_volumes: i32, + pub(crate) repeat: i32, + pub(crate) priority: i32, + pub(crate) private: bool, + pub(crate) notes: Option, + pub(crate) score: f32, + pub(crate) media: MediaEntry, } #[derive(Deserialize, Debug)] -struct MediaEntry { - id: i32, - title: MediaTitle, - status: String, - chapters: Option, - volumes: Option, +pub(crate) struct MediaEntry { + pub(crate) id: i32, + pub(crate) title: MediaTitle, + pub(crate) status: String, + pub(crate) chapters: Option, + pub(crate) volumes: Option, #[serde(rename = "coverImage")] - cover_image: CoverImage, + pub(crate) cover_image: CoverImage, #[serde(rename = "isAdult")] - is_adult: bool, + pub(crate) is_adult: bool, #[serde(rename = "isFavourite")] - is_favourite: bool, + pub(crate) is_favourite: bool, } #[derive(Deserialize, Debug)] -struct MediaTitle { - romaji: String, - english: Option, - native: String, +pub(crate) struct MediaTitle { + pub(crate) romaji: String, + pub(crate) english: Option, + pub(crate) native: String, #[serde(rename = "userPreferred")] - user_preferred: String, + pub(crate) user_preferred: String, } #[derive(Deserialize, Debug)] -struct CoverImage { - large: String, - medium: String, - color: Option, +pub(crate) struct CoverImage { + pub(crate) large: String, + pub(crate) medium: String, + pub(crate) color: Option, } // pub fn load_demo() -> MediaListCollection { diff --git a/mangades-plain/src/main.rs b/mangades-plain/src/main.rs index 4f6248c..677bfde 100644 --- a/mangades-plain/src/main.rs +++ b/mangades-plain/src/main.rs @@ -1,9 +1,12 @@ use std::sync::{Arc, mpsc, Mutex}; use mangui::nodes::layout::Layout; use mangui::{MainEntry, SharedNode}; -use mangui::femtovg::Paint; +use mangui::dpi::PhysicalPosition; +use mangui::events::InnerEvent; +use mangui::femtovg::{ImageFlags, Paint}; use mangui::nodes::text::Text; use mangui::nodes::{Style, TaffyStyle, ToShared}; +use mangui::nodes::image::{Image, ImageLoad}; use mangui::taffy::{AlignItems, FlexDirection, JustifyContent, LengthPercentage, LengthPercentageAuto, Point, Rect}; use uno_gen::uno; use crate::anilist::load_demo_async; @@ -45,8 +48,8 @@ async fn main() { let groot_clone = groot.clone(); tokio::spawn(async move { let data = load_demo_async().await; - - let mainview_container = Layout::default() + + let mut mainview_container = Layout::default() .style(Style { layout: TaffyStyle { flex_grow: 1., @@ -56,7 +59,26 @@ async fn main() { background: Some(Paint::color(*tokens::BACKGROUND)), ..Default::default() }) - .to_shared(); + .to_arcmutex(); + mainview_container.lock().unwrap().events.add_handler(Box::new({ + let mainview_container = mainview_container.clone(); + move |event| { + if let InnerEvent::Wheel { delta, .. } = event.event { + let delta = match delta { + mangui::events::MouseScrollDelta::LineDelta(_, y) => y * 30f32, + mangui::events::MouseScrollDelta::PixelDelta(PhysicalPosition { y, .. }) => y as f32, + }; + let mut layout = mainview_container.lock().unwrap(); + // layout.style.layout.scroll.y -= delta * 10.; + layout.style.scroll_y -= delta; + // cap scroll_y to 0 + if layout.style.scroll_y < 0. { + layout.style.scroll_y = 0.; + } + println!("scroll_y: {}", layout.style.scroll_y); + } + } + })); let i = LengthPercentageAuto::Length(5.); let title = Text::new("Mangades".to_owned(), TEXT_LARGE) .style(Style { @@ -64,16 +86,76 @@ async fn main() { ..uno!(p-10) }) .to_shared(); + append(&{ mainview_container.clone() }, &title); - append(&mainview_container, &title); + for list in data.lists { + let list_container = Layout::default() + .style(Style { + layout: TaffyStyle { + flex_grow: 1., + flex_direction: FlexDirection::Column, + ..Default::default() + }, + background: Some(Paint::color(*tokens::BACKGROUND)), + ..Default::default() + }) + .to_shared(); + let list_title = Text::new(list.name, TEXT_LARGE) + .style(Style { + text_fill: Some(Paint::color(*tokens::WHITE)), + ..uno!(p-10) + }) + .to_shared(); + append(&{ mainview_container.clone() }, &list_container); + append(&list_container, &list_title); + + for entry in list.entries { + let entry_container = Layout::default() + .style(Style { + layout: TaffyStyle { + flex_grow: 1., + flex_direction: FlexDirection::Row, + ..Default::default() + }, + background: Some(Paint::color(*tokens::BACKGROUND)), + ..Default::default() + }) + .to_shared(); + // image loading disabled for speed + // let addr = entry.media.cover_image.large; + // // use only last two parts from url, which is a folder and a file (either medium/something.jpg or large/something.jpg) + // let addr = addr.split('/').collect::>().into_iter().rev().take(2).collect::>().into_iter().rev().collect::>().join("/"); + // let addr = addr.replace("medium", "large").replace("small", "medium"); + // let addr = format!("demo/{}", addr); + // dbg!(&addr); + // let image = Image::new( + // ImageLoad::LoadFile(addr.parse().unwrap(), + // ImageFlags::empty()) + // ) + // .style(Style { + // ..Default::default() + // }) + // .to_shared(); + let title = Text::new(entry.media.title.user_preferred, TEXT_LARGE) + .style(Style { + text_fill: Some(Paint::color(*tokens::WHITE)), + ..uno!(p-10) + }) + .to_shared(); + append(&list_container, &entry_container); + // append(&entry_container, &image); + append(&entry_container, &title); + } + } + detach(&loading_container); - append(&groot_clone, &mainview_container); - + append(&groot_clone, &{ mainview_container.clone() }); + tx.send(()).unwrap(); }); mangui::run_event_loop(MainEntry { root: groot.clone(), render: rx - }); + }).unwrap(); } diff --git a/mangades/Cargo.toml b/mangades/Cargo.toml index be0571b..52dbd5f 100644 --- a/mangades/Cargo.toml +++ b/mangades/Cargo.toml @@ -8,4 +8,4 @@ edition = "2021" [dependencies] mangui = { path = "../ui"} rusalka = { path = "../rusalka"} -rusalka-macro = { path = "../rusalka-macro"} +rusalka-macro = { path = "../rusalka-macro"} \ No newline at end of file diff --git a/ui/src/lib.rs b/ui/src/lib.rs index c166540..646fa54 100644 --- a/ui/src/lib.rs +++ b/ui/src/lib.rs @@ -35,6 +35,7 @@ pub mod events; pub use taffy; pub use femtovg; pub use cosmic_text; +pub use winit::dpi; pub type CurrentRenderer = OpenGl; pub type SharedNode = Arc>; @@ -64,7 +65,7 @@ pub struct MainEntry { /// The event loop only returns when the window is closed, and all the resources regarding the window are freed. /// Note that the DOM tree may not be destroyed if you hold a reference to it, and the DOM tree can be used again, although it's discouraged - /// your app should exit at this point and only do cleanup. -pub fn run_event_loop(entry: MainEntry) -> () { +pub fn run_event_loop(entry: MainEntry) -> Result<(), winit::error::EventLoopError> { let event_loop = EventLoop::new().unwrap(); let (buffer_context, gl_display, window, surface) = create_window(&event_loop); @@ -107,9 +108,47 @@ pub fn run_event_loop(entry: MainEntry) -> () { let focus_path: Option> = None; let mut mouse_values: HashMap = HashMap::new(); - event_loop.run(move |event, target| match event { + let res = event_loop.run(move |event, target| match event { Event::WindowEvent { event, .. } => match event { - WindowEvent::MouseWheel { device_id: _, delta: _, phase: _, .. } => {}, + WindowEvent::MouseWheel { device_id, delta, phase } => { + let default = MouseValue { + last_location: Location::new(0., 0.), + buttons: 0 + }; + let mouse_value = mouse_values.get(&device_id) + .unwrap_or(&default); + + let path = get_element_at(&root, &context, mouse_value.last_location); + + if let Some(path) = path { + let target_layout = context.node_layout.get(path.last().unwrap()); + let target_layout = match target_layout { + Some(target_layout) => target_layout, + None => { return; } + }; + let target_layout = context.taffy.layout(target_layout.to_owned()).unwrap(); + let event = NodeEvent { + target: path.last().unwrap().clone(), + path: path.clone(), + event: events::InnerEvent::Wheel { + delta, + phase, + mouse: MouseEvent { + button: None, + buttons: mouse_value.buttons, + client: mouse_value.last_location, + movement: Location::new(0., 0.), + device: device_id, + modifiers, + offset: mouse_value.last_location - target_layout.location.into() + } + } + }; + + run_event_handlers(path, event); + window.request_redraw(); + } + }, WindowEvent::CursorMoved { device_id, position, .. } => { let mouse_value = mouse_values.get(&device_id); let (movement, location, mouse_value) = match mouse_value { @@ -168,9 +207,9 @@ pub fn run_event_loop(entry: MainEntry) -> () { match &focus_path { Some(path) => { let strong_focus_path: Option> = convert_vec_option_to_option_vec(path.iter().map(|weak| weak.upgrade()).collect()); - if matches!(strong_focus_path, None) { return; } + if strong_focus_path.is_none() { return; } let strong_focus_path = strong_focus_path.unwrap(); - if strong_focus_path.len() == 0 { return; } + if strong_focus_path.is_empty() { return; } let focus_event = NodeEvent { target: strong_focus_path.last().unwrap().clone(), @@ -197,7 +236,7 @@ pub fn run_event_loop(entry: MainEntry) -> () { WindowEvent::MouseInput { device_id, state, button, .. } => { let mouse_value = mouse_values.get(&device_id); let mut mouse_value = match mouse_value { - Some(mouse_value) => mouse_value.clone(), + Some(mouse_value) => *mouse_value, None => { return; } // Mouse move should be fired first }; mouse_value.update_buttons(button, state); @@ -205,36 +244,33 @@ pub fn run_event_loop(entry: MainEntry) -> () { let location = mouse_value.last_location; let path = get_element_at(&root, &context, location); - match path { - Some(path) => { - let target_layout = context.node_layout.get(path.last().unwrap()); - let target_layout = match target_layout { - Some(target_layout) => target_layout, - None => { return; } - }; - let target_layout = context.taffy.layout(target_layout.to_owned()).unwrap(); - let mevent = MouseEvent { - button: Some(button), - buttons: mouse_value.buttons, - client: location, - movement: Location::new(0., 0.), - device: device_id, - modifiers, - offset: location - target_layout.location.into() - }; - let event = NodeEvent { - target: path.last().unwrap().clone(), - path: path.clone(), - event: match state { - winit::event::ElementState::Pressed => events::InnerEvent::MouseDown(mevent), - winit::event::ElementState::Released => events::InnerEvent::MouseUp(mevent) - } - }; + if let Some(path) = path { + let target_layout = context.node_layout.get(path.last().unwrap()); + let target_layout = match target_layout { + Some(target_layout) => target_layout, + None => { return; } + }; + let target_layout = context.taffy.layout(target_layout.to_owned()).unwrap(); + let mevent = MouseEvent { + button: Some(button), + buttons: mouse_value.buttons, + client: location, + movement: Location::new(0., 0.), + device: device_id, + modifiers, + offset: location - target_layout.location.into() + }; + let event = NodeEvent { + target: path.last().unwrap().clone(), + path: path.clone(), + event: match state { + winit::event::ElementState::Pressed => events::InnerEvent::MouseDown(mevent), + winit::event::ElementState::Released => events::InnerEvent::MouseUp(mevent) + } + }; - window.request_redraw(); - run_event_handlers(path, event); - }, - None => {} + window.request_redraw(); + run_event_handlers(path, event); } }, WindowEvent::CloseRequested => target.exit(), @@ -299,7 +335,7 @@ pub fn run_event_loop(entry: MainEntry) -> () { // dbg!("recomputed"); } // Clear the render queue - while let Ok(_) = entry.render.try_recv() {} + while entry.render.try_recv().is_ok() {} render(&buffer_context, &surface, &window, &mut context, &root); } _ => {} @@ -310,7 +346,7 @@ pub fn run_event_loop(entry: MainEntry) -> () { // dbg!(refresh_rate); // some leeway before vsync // target.set_control_flow(ControlFlow::wait_duration(Duration::from_millis(1000 / refresh_rate as u64 - 100/refresh_rate as u64))); - if let Ok(_) = entry.render.try_recv() { + if entry.render.try_recv().is_ok() { window.request_redraw(); } // } @@ -318,7 +354,8 @@ pub fn run_event_loop(entry: MainEntry) -> () { }, // In the future, window should be created after resuming from suspend (for android support) _ => {} - }).unwrap(); + }); + res } /// I have no idea if there's a better way to do this in rust... diff --git a/ui/src/nodes/layout.rs b/ui/src/nodes/layout.rs index f1f96bb..8a58307 100644 --- a/ui/src/nodes/layout.rs +++ b/ui/src/nodes/layout.rs @@ -1,6 +1,4 @@ use std::fmt::{Debug, Formatter}; -use std::sync::{Arc, RwLock}; -use femtovg::{Paint, Path}; use crate::{nodes::{Node, NodeChildren, Style}, events::handler::EventHandlerDatabase, WeakNode, SharedNode}; use taffy::style::Dimension; use crate::nodes::primitives::draw_rect; diff --git a/ui/src/nodes/mod.rs b/ui/src/nodes/mod.rs index 4f204d6..7c08389 100644 --- a/ui/src/nodes/mod.rs +++ b/ui/src/nodes/mod.rs @@ -77,7 +77,15 @@ pub struct Style { /// border radius in pixels pub border_radius: f32, /// Various transformation (position, scale and rotation) - pub transform: Option + pub transform: Option, + /// sets scroll offset for x-axis + /// 0.0 is the default value + /// you cannot scroll outside the layout - render function will clip the value in that case + pub scroll_x: f32, + /// sets scroll offset for y-axis + /// 0.0 is the default value + /// you cannot scroll outside the layout - render function will clip the value in that case + pub scroll_y: f32, } type NodeChildren = Vec; @@ -295,6 +303,9 @@ pub trait Node: Debug + Send { pub trait ToShared { fn to_shared(self) -> SharedNode; + fn to_arcmutex(self) -> Arc> where Self: Sized { + Arc::new(Mutex::new(self)) + } } impl ToShared for T { @@ -397,13 +408,20 @@ pub(crate) fn render_recursively(node: &SharedNode, context: &mut RenderContext) let sself = node.clone(); context.canvas.save(); let offset = styles.transform.as_ref().map(|t| (t.position.x, t.position.y)).unwrap_or((0., 0.)); - context.canvas.translate(layout.location.x + offset.0, layout.location.y + offset.1); + let scroll_offset = (styles.scroll_x, styles.scroll_y); + let content_size = layout.content_size; + let visible_size = layout.size; + let scroll_offset = (scroll_offset.0.min(content_size.width - visible_size.width).max(0.), scroll_offset.1.min(content_size.height - visible_size.height).max(0.)); + context.canvas.translate( + layout.location.x + offset.0 - scroll_offset.0, + layout.location.y + offset.1 - scroll_offset.1 + ); if let Some(transform) = &styles.transform { context.canvas.scale(transform.scale.width, transform.scale.height); context.canvas.rotate(transform.rotation); } - let clip_width = matches!(styles.layout.overflow.x, Overflow::Hidden | Overflow::Clip); - let clip_height = matches!(styles.layout.overflow.y, Overflow::Hidden | Overflow::Clip); + let clip_width = matches!(styles.layout.overflow.x, Overflow::Hidden | Overflow::Clip | Overflow::Scroll); + let clip_height = matches!(styles.layout.overflow.y, Overflow::Hidden | Overflow::Clip | Overflow::Scroll); if clip_width || clip_height { context.canvas.scissor( 0., @@ -413,13 +431,14 @@ pub(crate) fn render_recursively(node: &SharedNode, context: &mut RenderContext) ); } drop(read_node); - sself.lock().unwrap().render_pre_children(context, layout); - if let Some(children) = sself.lock().unwrap().children() { + let mut locked = sself.lock().unwrap(); + locked.render_pre_children(context, layout); + if let Some(children) = locked.children() { for child in children { render_recursively(child, context); } } - sself.lock().unwrap().render_post_children(context, layout); + locked.render_post_children(context, layout); context.canvas.restore(); } diff --git a/uno-gen/src/lib.rs b/uno-gen/src/lib.rs index 968aea3..1d5dd9f 100644 --- a/uno-gen/src/lib.rs +++ b/uno-gen/src/lib.rs @@ -42,7 +42,7 @@ macro_rules! impl_enum_totokens { )? }); } - } + } } } @@ -76,7 +76,7 @@ macro_rules! impl_struct_usersettable_totokens { } }); } - } + } } } @@ -106,7 +106,7 @@ impl UserSettable { } } } - + fn is_empty(&self) -> bool { match self { UserSettable::Value(_) => false, @@ -831,6 +831,14 @@ fn process_rules(rules: Vec) -> Result { let value = value.to_user_settable(name_span, inverse)?; style.layout.require_non_arbitrary()?.overflow.require_non_arbitrary()?.y = value; }, + "rounded" => { + if let Some(value) = value { + let value = value.to_user_settable(name_span, inverse)?; + style.border_radius = value; + } else { + style.border_radius = UserSettable::Value(8.); + } + }, "layout" => { if let Some(value) = value { let value = value.to_user_settable(name_span, inverse)?;