diff --git a/bin/app/Cargo.toml b/bin/app/Cargo.toml
index 4ef2926bf..080eac6fe 100644
--- a/bin/app/Cargo.toml
+++ b/bin/app/Cargo.toml
@@ -76,6 +76,7 @@ enable-netdebug = []
schema-app = []
# Dev schema for testing
schema-test = []
+schema-test-scroll-layer = []
[patch.crates-io]
halo2_proofs = { git="https://github.com/parazyd/halo2", branch="v032" }
diff --git a/bin/app/Makefile b/bin/app/Makefile
index d0e44e737..39384a843 100644
--- a/bin/app/Makefile
+++ b/bin/app/Makefile
@@ -22,7 +22,7 @@ DEBUG_FEATURES = --features=enable-filelog,enable-plugins
#DEV_FEATURES = --features=enable-filelog,enable-netdebug,emulate-android
#DEV_FEATURES = --features=enable-filelog,enable-netdebug,enable-plugins
-DEV_FEATURES = --features=schema-app,enable-netdebug
+DEV_FEATURES = --features=schema-test-scroll-layer,enable-netdebug
default: build-release
./darkfi-app
diff --git a/bin/app/src/app/mod.rs b/bin/app/src/app/mod.rs
index 4dc14fdd1..0e88ca26a 100644
--- a/bin/app/src/app/mod.rs
+++ b/bin/app/src/app/mod.rs
@@ -141,6 +141,9 @@ impl App {
#[cfg(feature = "schema-test")]
schema::test::make(&self, window.clone(), &i18n_fish).await;
+ #[cfg(feature = "schema-test-scroll-layer")]
+ schema::test_scroll_layer::make(&self, window.clone(), &i18n_fish).await;
+
#[cfg(all(feature = "schema-app", feature = "schema-test"))]
compile_error!("Only one schema can be selected");
diff --git a/bin/app/src/app/schema/mod.rs b/bin/app/src/app/schema/mod.rs
index b01b98969..86a07740e 100644
--- a/bin/app/src/app/schema/mod.rs
+++ b/bin/app/src/app/schema/mod.rs
@@ -39,6 +39,7 @@ mod chat;
mod menu;
//mod settings;
pub mod test;
+pub mod test_scroll_layer;
macro_rules! i { ($($arg:tt)*) => { info!(target: "app::schema", $($arg)*); } }
macro_rules! e { ($($arg:tt)*) => { error!(target: "app::schema", $($arg)*); } }
diff --git a/bin/app/src/app/schema/test_scroll_layer.rs b/bin/app/src/app/schema/test_scroll_layer.rs
new file mode 100644
index 000000000..651dbc6a3
--- /dev/null
+++ b/bin/app/src/app/schema/test_scroll_layer.rs
@@ -0,0 +1,76 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2026 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+#![allow(unused_imports, unused_variables, dead_code)]
+
+use crate::{
+ app::{
+ node::{create_chatview, create_layer, create_text, create_vector_art, create_video},
+ App,
+ },
+ expr::{self, Compiler},
+ mesh::COLOR_PURPLE,
+ prop::{PropertyAtomicGuard, PropertyFloat32, Role},
+ scene::SceneNodePtr,
+ ui::{ChatView, Layer, Text, VectorArt, VectorShape, Video},
+ util::i18n::I18nBabelFish,
+};
+
+#[allow(dead_code)]
+pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
+ let atom = &mut PropertyAtomicGuard::none();
+
+ // Create a layer called view
+ let layer_node = create_layer("view");
+ let prop = layer_node.get_property("rect").unwrap();
+ prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+ prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+ prop.set_expr(atom, Role::App, 2, expr::load_var("w")).unwrap();
+ prop.set_expr(atom, Role::App, 3, expr::load_var("h")).unwrap();
+ layer_node.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
+ let layer_node = layer_node.setup(|me| Layer::new(me, app.renderer.clone())).await;
+ window.link(layer_node.clone());
+
+ // Create a bg mesh
+ let node = create_vector_art("bg");
+ let prop = node.get_property("rect").unwrap();
+ prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+ prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+ prop.set_expr(atom, Role::App, 2, expr::load_var("w")).unwrap();
+ prop.set_expr(atom, Role::App, 3, expr::load_var("h")).unwrap();
+ node.set_property_u32(atom, Role::App, "z_index", 0).unwrap();
+
+ let mut shape = VectorShape::new();
+ shape.add_filled_box(
+ expr::const_f32(0.),
+ expr::const_f32(0.),
+ expr::load_var("w"),
+ expr::load_var("h"),
+ [0., 0., 0., 1.],
+ );
+ shape.add_outline(
+ expr::const_f32(200.),
+ expr::const_f32(200.),
+ expr::const_f32(1000.),
+ expr::const_f32(1000.),
+ 1.,
+ COLOR_PURPLE,
+ );
+ let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+ layer_node.link(node);
+}
diff --git a/bin/app/src/scene.rs b/bin/app/src/scene.rs
index c0589ac34..857e95757 100644
--- a/bin/app/src/scene.rs
+++ b/bin/app/src/scene.rs
@@ -568,6 +568,7 @@ pub enum Pimpl {
Null,
Window(ui::WindowPtr),
Layer(ui::LayerPtr),
+ ScrollLayer(ui::ScrollLayerPtr),
VectorArt(ui::VectorArtPtr),
Text(ui::TextPtr),
Edit(ui::BaseEditPtr),
diff --git a/bin/app/src/ui/mod.rs b/bin/app/src/ui/mod.rs
index 0ef5b21ec..177e4e1e7 100644
--- a/bin/app/src/ui/mod.rs
+++ b/bin/app/src/ui/mod.rs
@@ -52,6 +52,8 @@ pub use vector_art::{
};
mod layer;
pub use layer::{Layer, LayerPtr};
+mod scroll_layer;
+pub use scroll_layer::{ScrollLayer, ScrollLayerPtr};
mod shortcut;
pub use shortcut::{Shortcut, ShortcutPtr};
mod menu;
@@ -209,6 +211,7 @@ impl OnModify {
pub fn get_ui_object_ptr(node: &SceneNode3) -> Arc {
match node.pimpl() {
Pimpl::Layer(obj) => obj.clone(),
+ Pimpl::ScrollLayer(obj) => obj.clone(),
Pimpl::VectorArt(obj) => obj.clone(),
Pimpl::Text(obj) => obj.clone(),
Pimpl::Edit(obj) => obj.clone(),
@@ -226,6 +229,7 @@ pub fn get_ui_object_ptr(node: &SceneNode3) -> Arc {
pub fn get_ui_object3<'a>(node: &'a SceneNode3) -> &'a dyn UIObject {
match node.pimpl() {
Pimpl::Layer(obj) => obj.as_ref(),
+ Pimpl::ScrollLayer(obj) => obj.as_ref(),
Pimpl::VectorArt(obj) => obj.as_ref(),
Pimpl::Text(obj) => obj.as_ref(),
Pimpl::Edit(obj) => obj.as_ref(),
diff --git a/bin/app/src/ui/scroll_layer.rs b/bin/app/src/ui/scroll_layer.rs
new file mode 100644
index 000000000..c757b1f44
--- /dev/null
+++ b/bin/app/src/ui/scroll_layer.rs
@@ -0,0 +1,128 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2026 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+use async_trait::async_trait;
+use miniquad::{KeyCode, KeyMods, MouseButton, TouchPhase};
+use std::sync::Arc;
+
+use crate::{
+ gfx::{DrawCall, Point, Rectangle, RendererSync},
+ prop::{BatchGuardPtr, PropertyAtomicGuard},
+ scene::{Pimpl, SceneNodeWeak},
+ util::i18n::I18nBabelFish,
+ ExecutorPtr,
+};
+
+use super::{DrawUpdate, Layer, LayerPtr, UIObject};
+
+pub type ScrollLayerPtr = Arc;
+
+pub struct ScrollLayer {
+ inner: LayerPtr,
+}
+
+impl ScrollLayer {
+ pub async fn new(node: SceneNodeWeak, renderer: crate::gfx::Renderer) -> Pimpl {
+ let layer = Layer::new(node.clone(), renderer).await;
+ let inner = match layer {
+ Pimpl::Layer(l) => l,
+ _ => unreachable!(),
+ };
+
+ Pimpl::ScrollLayer(Arc::new(Self { inner }))
+ }
+}
+
+#[async_trait]
+impl UIObject for ScrollLayer {
+ fn priority(&self) -> u32 {
+ self.inner.priority()
+ }
+
+ fn init(&self) {
+ self.inner.init();
+ }
+
+ async fn start(self: Arc, ex: ExecutorPtr) {
+ self.inner.clone().start(ex).await;
+ }
+
+ fn stop(&self) {
+ self.inner.stop();
+ }
+
+ async fn draw(
+ &self,
+ parent_rect: Rectangle,
+ atom: &mut PropertyAtomicGuard,
+ ) -> Option {
+ self.inner.draw(parent_rect, atom).await
+ }
+
+ async fn handle_char(&self, key: char, mods: KeyMods, repeat: bool) -> bool {
+ self.inner.handle_char(key, mods, repeat).await
+ }
+
+ async fn handle_key_down(&self, key: KeyCode, mods: KeyMods, repeat: bool) -> bool {
+ self.inner.handle_key_down(key, mods, repeat).await
+ }
+
+ async fn handle_key_up(&self, key: KeyCode, mods: KeyMods) -> bool {
+ self.inner.handle_key_up(key, mods).await
+ }
+
+ async fn handle_mouse_btn_down(&self, btn: MouseButton, mouse_pos: Point) -> bool {
+ self.inner.handle_mouse_btn_down(btn, mouse_pos).await
+ }
+
+ async fn handle_mouse_btn_up(&self, btn: MouseButton, mouse_pos: Point) -> bool {
+ self.inner.handle_mouse_btn_up(btn, mouse_pos).await
+ }
+
+ async fn handle_mouse_move(&self, mouse_pos: Point) -> bool {
+ self.inner.handle_mouse_move(mouse_pos).await
+ }
+
+ async fn handle_mouse_wheel(&self, wheel_pos: Point) -> bool {
+ self.inner.handle_mouse_wheel(wheel_pos).await
+ }
+
+ async fn handle_touch(&self, phase: TouchPhase, id: u64, touch_pos: Point) -> bool {
+ self.inner.handle_touch(phase, id, touch_pos).await
+ }
+
+ fn handle_touch_sync(
+ &self,
+ renderer: &RendererSync,
+ phase: TouchPhase,
+ id: u64,
+ touch_pos: Point,
+ ) -> bool {
+ self.inner.handle_touch_sync(renderer, phase, id, touch_pos)
+ }
+
+ fn set_i18n(&self, i18n_fish: &I18nBabelFish) {
+ self.inner.set_i18n(i18n_fish);
+ }
+}
+
+impl std::fmt::Debug for ScrollLayer {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ self.inner.fmt(f)
+ }
+}