add scrolling capability

This commit is contained in:
Daniel Bulant 2024-03-14 21:59:55 +01:00
parent 36065e3221
commit 8875cbb6ff
7 changed files with 240 additions and 96 deletions

View file

@ -5,74 +5,74 @@ use mangui::nodes::image::ImageLoad;
#[derive(Deserialize, Debug)]
struct GraphqlResponse<T> {
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<MediaList>
pub(crate) lists: Vec<MediaList>
}
#[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<MediaListEntry>,
pub(crate) is_split_completed_list: bool,
pub(crate) entries: Vec<MediaListEntry>,
}
#[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<String>,
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<String>,
pub(crate) score: f32,
pub(crate) media: MediaEntry,
}
#[derive(Deserialize, Debug)]
struct MediaEntry {
id: i32,
title: MediaTitle,
status: String,
chapters: Option<i32>,
volumes: Option<i32>,
pub(crate) struct MediaEntry {
pub(crate) id: i32,
pub(crate) title: MediaTitle,
pub(crate) status: String,
pub(crate) chapters: Option<i32>,
pub(crate) volumes: Option<i32>,
#[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<String>,
native: String,
pub(crate) struct MediaTitle {
pub(crate) romaji: String,
pub(crate) english: Option<String>,
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<String>,
pub(crate) struct CoverImage {
pub(crate) large: String,
pub(crate) medium: String,
pub(crate) color: Option<String>,
}
// pub fn load_demo() -> MediaListCollection {

View file

@ -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::<Vec<&str>>().into_iter().rev().take(2).collect::<Vec<&str>>().into_iter().rev().collect::<Vec<&str>>().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();
}

View file

@ -8,4 +8,4 @@ edition = "2021"
[dependencies]
mangui = { path = "../ui"}
rusalka = { path = "../rusalka"}
rusalka-macro = { path = "../rusalka-macro"}
rusalka-macro = { path = "../rusalka-macro"}

View file

@ -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<Mutex<dyn Node>>;
@ -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<Vec<WeakNode>> = None;
let mut mouse_values: HashMap<DeviceId, MouseValue> = 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<Vec<SharedNode>> = 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...

View file

@ -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;

View file

@ -77,7 +77,15 @@ pub struct Style {
/// border radius in pixels
pub border_radius: f32,
/// Various transformation (position, scale and rotation)
pub transform: Option<Transform>
pub transform: Option<Transform>,
/// 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<SharedNode>;
@ -295,6 +303,9 @@ pub trait Node: Debug + Send {
pub trait ToShared {
fn to_shared(self) -> SharedNode;
fn to_arcmutex(self) -> Arc<Mutex<Self>> where Self: Sized {
Arc::new(Mutex::new(self))
}
}
impl<T: Node + 'static> 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();
}

View file

@ -42,7 +42,7 @@ macro_rules! impl_enum_totokens {
)?
});
}
}
}
}
}
@ -76,7 +76,7 @@ macro_rules! impl_struct_usersettable_totokens {
}
});
}
}
}
}
}
@ -106,7 +106,7 @@ impl<T> UserSettable<T> {
}
}
}
fn is_empty(&self) -> bool {
match self {
UserSettable::Value(_) => false,
@ -831,6 +831,14 @@ fn process_rules(rules: Vec<Rule>) -> Result<Style, RuleParseError> {
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)?;