GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
Create ScrollBar widget and integrate automatically into Page objects
src/context.rs | 28 ++++-
src/widget/container/container.rs | 19 +++
src/widget/container/mod.rs | 2 +
src/widget/container/page.rs | 174 +++++++++++++++++++++++---
src/widget/container/scroll_bar.rs | 250 +++++++++++++++++++++++++++++++++++++
src/widget/mod.rs | 2 +-
6 files changed, 459 insertions(+), 16 deletions(-)
diff --git a/src/context.rs b/src/context.rs
index d8e6337..14314d4 100644
--- a/src/context.rs
+++ b/src/context.rs
@@ -34,6 +34,32 @@ impl UiContext {
return false;
}
unsafe {
+ let mut out_of_bounds = false;
+ if (*root).is_page() {
+ let mut scrollbar_dragging = false;
+ if let Some(page) = (*root).as_any().downcast_ref::<crate::widget::Page>() {
+ if page.scroll_bar.dragging {
+ scrollbar_dragging = true;
+ }
+ }
+
+ if !scrollbar_dragging {
+ if let Event::PointerMove { x, y }
+ | Event::MouseButton { x, y, .. }
+ | Event::MouseWheel { delta: _, x, y } = event
+ {
+ let (rx, ry, rw, rh) = (*root).rect();
+ if *x < rx || *x > rx + rw || *y < ry || *y > ry + rh {
+ out_of_bounds = true;
+ }
+ }
+ }
+ }
+
+ if out_of_bounds {
+ return false;
+ }
+
// For KeyInput, send directly to focused widget if it exists
if let Event::KeyInput(_) = event {
if let Some(focused) = self.focused_widget {
@@ -48,7 +74,7 @@ impl UiContext {
let children = (*root).children(self);
match event {
- Event::PointerMove { .. } => {
+ Event::PointerMove { .. } | Event::Tick(_) => {
for child in children.into_iter().rev() {
if self.propagate_event(event, child) {
handled = true;
diff --git a/src/widget/container/container.rs b/src/widget/container/container.rs
index 9533484..132cc08 100644
--- a/src/widget/container/container.rs
+++ b/src/widget/container/container.rs
@@ -24,6 +24,25 @@ impl Element for Container {
self as *mut Self as *mut (dyn Element + 'static)
}
+ fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
+ let dx = x - self.base.x;
+ let dy = y - self.base.y;
+
+ self.base.x = x;
+ self.base.y = y;
+ self.base.w = w;
+ self.base.h = h;
+
+ if dx.abs() > 0.001 || dy.abs() > 0.001 {
+ for &child in &self.children {
+ unsafe {
+ let (cx, cy, cw, ch) = (*child).rect();
+ (*child).set_rect(cx + dx, cy + dy, cw, ch);
+ }
+ }
+ }
+ }
+
fn measure(&self, constraints: LayoutConstraints, ctx: &UiContext) -> Size {
let mut max_w = 0.0f32;
let mut max_h = 0.0f32;
diff --git a/src/widget/container/mod.rs b/src/widget/container/mod.rs
index abbff2b..53fbb89 100644
--- a/src/widget/container/mod.rs
+++ b/src/widget/container/mod.rs
@@ -14,6 +14,7 @@ pub mod layer;
pub mod page;
pub mod backplate;
pub mod paginator;
+pub mod scroll_bar;
pub use container::Container;
pub use header::Header;
@@ -31,3 +32,4 @@ pub use layer::Layer;
pub use page::Page;
pub use backplate::Backplate;
pub use paginator::Paginator;
+pub use scroll_bar::ScrollBar;
diff --git a/src/widget/container/page.rs b/src/widget/container/page.rs
index 292a850..e5f16bf 100644
--- a/src/widget/container/page.rs
+++ b/src/widget/container/page.rs
@@ -4,7 +4,7 @@ use crate::widget::display::TextLabel;
use super::layer::Layer;
pub trait PageLayout {
- fn layout(&self, x: f32, y: f32, w: f32, h: f32, children: &[*mut (dyn Element + 'static)]);
+ fn layout(&self, x: f32, y: f32, w: f32, h: f32, children: &[*mut (dyn Element + 'static)]) -> f32;
}
#[derive(Debug, Clone, Copy)]
@@ -25,7 +25,7 @@ impl Default for VerticalLayout {
}
impl PageLayout for VerticalLayout {
- fn layout(&self, x: f32, y: f32, w: f32, _h: f32, children: &[*mut (dyn Element + 'static)]) {
+ fn layout(&self, x: f32, y: f32, w: f32, _h: f32, children: &[*mut (dyn Element + 'static)]) -> f32 {
let padding_x = self.padding_x;
let padding_y = self.padding_y;
let left_x = x + padding_x;
@@ -40,6 +40,7 @@ impl PageLayout for VerticalLayout {
child.set_rect(left_x, current_y, available_w, use_h);
current_y += use_h + spacing;
}
+ current_y - y
}
}
@@ -61,10 +62,10 @@ impl Default for ColumnsLayout {
}
impl PageLayout for ColumnsLayout {
- fn layout(&self, x: f32, y: f32, w: f32, h: f32, children: &[*mut (dyn Element + 'static)]) {
+ fn layout(&self, x: f32, y: f32, w: f32, h: f32, children: &[*mut (dyn Element + 'static)]) -> f32 {
let count = children.len();
if count == 0 {
- return;
+ return 0.0;
}
let total_spacing = self.spacing * (count - 1) as f32;
let total_padding = self.padding_x * 2.0;
@@ -79,6 +80,7 @@ impl PageLayout for ColumnsLayout {
child.set_rect(current_x, start_y, col_w, use_h);
current_x += col_w + self.spacing;
}
+ use_h + 2.0 * self.padding_y
}
}
@@ -87,6 +89,9 @@ pub struct Page {
pub visible: bool,
pub owned_children: Vec<Box<dyn Element>>,
pub layout: Box<dyn PageLayout>,
+ pub scroll_y: f32,
+ pub content_h: f32,
+ pub scroll_bar: ScrollBar,
}
impl std::fmt::Debug for Page {
@@ -94,6 +99,8 @@ impl std::fmt::Debug for Page {
f.debug_struct("Page")
.field("base", &self.base)
.field("visible", &self.visible)
+ .field("scroll_y", &self.scroll_y)
+ .field("content_h", &self.content_h)
.finish()
}
}
@@ -105,6 +112,9 @@ impl Page {
visible: true,
owned_children: Vec::new(),
layout: Box::new(VerticalLayout::default()),
+ scroll_y: 0.0,
+ content_h: 0.0,
+ scroll_bar: ScrollBar::new(),
}
}
@@ -125,6 +135,28 @@ impl Page {
}
}
+fn clip_quad(
+ quad: (f32, f32, f32, f32, [f32; 4]),
+ bounds: (f32, f32, f32, f32),
+) -> Option<(f32, f32, f32, f32, [f32; 4])> {
+ let (qx, qy, qw, qh, qc) = quad;
+ let (bx, by, bw, bh) = bounds;
+
+ let x1 = qx.max(bx);
+ let y1 = qy.max(by);
+ let x2 = (qx + qw).min(bx + bw);
+ let y2 = (qy + qh).min(by + bh);
+
+ let w = x2 - x1;
+ let h = y2 - y1;
+
+ if w > 0.0 && h > 0.0 {
+ Some((x1, y1, w, h, qc))
+ } else {
+ None
+ }
+}
+
impl Element for Page {
fn base(&self) -> Option<&Widget> { Some(&self.base.base) }
fn is_page(&self) -> bool { true }
@@ -148,7 +180,30 @@ impl Element for Page {
if !self.visible {
return;
}
- self.layout.layout(x, y, w, h, &self.base.children);
+ let content_h = self.layout.layout(x, y, w, h, &self.base.children);
+ self.content_h = content_h;
+
+ let max_scroll = (content_h - h).max(0.0);
+ self.scroll_y = self.scroll_y.clamp(0.0, max_scroll);
+
+ // Position the scroll bar
+ let sb_w = 6.0;
+ let sb_padding = 2.0;
+ let sb_x = x + w - sb_w - sb_padding;
+ self.scroll_bar.set_rect(sb_x, y + 4.0, sb_w, h - 8.0);
+ self.scroll_bar.update(self.scroll_y, content_h, h);
+
+ let self_ptr = self as *mut Page as *mut (dyn Element + 'static);
+ self.scroll_bar.parent = Some(self_ptr);
+
+ if self.scroll_y > 0.0 {
+ // Apply scroll offset to children
+ for &child_ptr in &self.base.children {
+ let child = unsafe { &mut *child_ptr };
+ let (cx, cy, cw, ch) = child.rect();
+ child.set_rect(cx, cy - self.scroll_y, cw, ch);
+ }
+ }
}
fn color(&self) -> [f32; 4] {
@@ -157,7 +212,6 @@ impl Element for Page {
c
}
-
fn visible(&self) -> bool {
self.visible
}
@@ -184,7 +238,13 @@ impl Element for Page {
}
fn children(&self, ctx: &UiContext) -> Vec<*mut (dyn Element + 'static)> {
- self.base.children(ctx)
+ let mut list = self.base.children(ctx);
+ let (_, _, _, h) = self.rect();
+ if self.content_h > h {
+ let sb_ptr = &self.scroll_bar as *const ScrollBar as *mut ScrollBar as *mut (dyn Element + 'static);
+ list.push(sb_ptr);
+ }
+ list
}
fn set_parent(&mut self, parent: Option<*mut (dyn Element + 'static)>, ctx: &mut UiContext) {
@@ -201,34 +261,96 @@ impl Element for Page {
}
let mut quads = Vec::new();
let c = self.color();
+ let bounds = self.rect();
if c[3] > 0.0 {
- let (x, y, w, h) = self.rect();
- quads.push((x, y, w, h, c));
+ quads.push((bounds.0, bounds.1, bounds.2, bounds.3, c));
+ }
+
+ for q in self.base.all_quads(ctx) {
+ if let Some(clipped) = clip_quad(q, bounds) {
+ quads.push(clipped);
+ }
+ }
+
+ if self.content_h > bounds.3 {
+ quads.extend(self.scroll_bar.all_quads(ctx));
}
- quads.extend(self.base.all_quads(ctx));
+
quads
}
-
fn text_labels(&self) -> Vec<TextLabel> {
if !self.visible {
return Vec::new();
}
- self.base.text_labels()
+ let (_, py, _, ph) = self.rect();
+ let mut result = Vec::new();
+ for label in self.base.text_labels() {
+ if label.y >= py && label.y + label.font_size <= py + ph {
+ result.push(label);
+ }
+ }
+ result
}
fn text_labels_with_bounds(&self, ctx: &UiContext) -> Vec<(TextLabel, Option<[f32; 4]>)> {
if !self.visible {
return Vec::new();
}
- self.base.text_labels_with_bounds(ctx)
+ let (px, py, pw, ph) = self.rect();
+ let page_bounds = [px, py, px + pw, py + ph];
+
+ let mut result = Vec::new();
+ for (label, bounds) in self.base.text_labels_with_bounds(ctx) {
+ let intersected_bounds = if let Some([l, t, r, b]) = bounds {
+ let il = l.max(page_bounds[0]);
+ let it = t.max(page_bounds[1]);
+ let ir = r.min(page_bounds[2]);
+ let ib = b.min(page_bounds[3]);
+ if il < ir && it < ib {
+ Some([il, it, ir, ib])
+ } else {
+ continue;
+ }
+ } else {
+ if label.y + label.font_size < py || label.y > py + ph {
+ continue;
+ }
+ Some(page_bounds)
+ };
+ result.push((label, intersected_bounds));
+ }
+ result
}
fn text_labels_with_font_and_bounds(&self, ctx: &UiContext) -> Vec<(TextLabel, Option<String>, Option<[f32; 4]>)> {
if !self.visible {
return Vec::new();
}
- self.base.text_labels_with_font_and_bounds(ctx)
+ let (px, py, pw, ph) = self.rect();
+ let page_bounds = [px, py, px + pw, py + ph];
+
+ let mut result = Vec::new();
+ for (label, font, bounds) in self.base.text_labels_with_font_and_bounds(ctx) {
+ let intersected_bounds = if let Some([l, t, r, b]) = bounds {
+ let il = l.max(page_bounds[0]);
+ let it = t.max(page_bounds[1]);
+ let ir = r.min(page_bounds[2]);
+ let ib = b.min(page_bounds[3]);
+ if il < ir && it < ib {
+ Some([il, it, ir, ib])
+ } else {
+ continue;
+ }
+ } else {
+ if label.y + label.font_size < py || label.y > py + ph {
+ continue;
+ }
+ Some(page_bounds)
+ };
+ result.push((label, font, intersected_bounds));
+ }
+ result
}
fn get_text_items(&self) -> Vec<(&glyphon::Buffer, f32, f32, glyphon::Color)> {
@@ -244,4 +366,28 @@ impl Element for Page {
}
self.base.hit_test(px, py, ctx)
}
+
+ fn mouse_wheel(&mut self, delta: &MouseScrollDelta, px: f32, py: f32, ctx: &mut UiContext) -> bool {
+ if !self.visible {
+ return false;
+ }
+ if self.hit_test(px, py, ctx) {
+ let scroll_speed = 24.0;
+ let dy = match delta {
+ MouseScrollDelta::LineDelta(_, y) => -y * scroll_speed,
+ MouseScrollDelta::PixelDelta(pos) => -pos.y as f32,
+ };
+ let old_scroll = self.scroll_y;
+ let (x, y, w, h) = self.rect();
+ let max_scroll = (self.content_h - h).max(0.0);
+ self.scroll_y = (self.scroll_y + dy).clamp(0.0, max_scroll);
+ self.scroll_bar.scroll_y = self.scroll_y;
+ if (self.scroll_y - old_scroll).abs() > 0.01 {
+ self.set_rect(x, y, w, h);
+ self.mark_dirty(ctx);
+ return true;
+ }
+ }
+ false
+ }
}
diff --git a/src/widget/container/scroll_bar.rs b/src/widget/container/scroll_bar.rs
new file mode 100644
index 0000000..8cf9262
--- /dev/null
+++ b/src/widget/container/scroll_bar.rs
@@ -0,0 +1,250 @@
+use crate::widget::*;
+use crate::context::UiContext;
+
+pub struct ScrollBar {
+ x: f32,
+ y: f32,
+ w: f32,
+ h: f32,
+ pub scroll_y: f32,
+ pub content_h: f32,
+ pub viewport_h: f32,
+ pub hovered: bool,
+ pub dragging: bool,
+ pub parent: Option<*mut (dyn Element + 'static)>,
+ pub children: Vec<*mut (dyn Element + 'static)>,
+}
+
+impl std::fmt::Debug for ScrollBar {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("ScrollBar")
+ .field("x", &self.x)
+ .field("y", &self.y)
+ .field("w", &self.w)
+ .field("h", &self.h)
+ .field("scroll_y", &self.scroll_y)
+ .field("content_h", &self.content_h)
+ .field("viewport_h", &self.viewport_h)
+ .field("hovered", &self.hovered)
+ .field("dragging", &self.dragging)
+ .finish()
+ }
+}
+
+impl Clone for ScrollBar {
+ fn clone(&self) -> Self {
+ Self {
+ x: self.x,
+ y: self.y,
+ w: self.w,
+ h: self.h,
+ scroll_y: self.scroll_y,
+ content_h: self.content_h,
+ viewport_h: self.viewport_h,
+ hovered: self.hovered,
+ dragging: self.dragging,
+ parent: self.parent,
+ children: self.children.clone(),
+ }
+ }
+}
+
+impl ScrollBar {
+ pub fn new() -> Self {
+ Self {
+ x: 0.0,
+ y: 0.0,
+ w: 6.0,
+ h: 0.0,
+ scroll_y: 0.0,
+ content_h: 0.0,
+ viewport_h: 0.0,
+ hovered: false,
+ dragging: false,
+ parent: None,
+ children: Vec::new(),
+ }
+ }
+
+ pub fn update(&mut self, scroll_y: f32, content_h: f32, viewport_h: f32) {
+ self.scroll_y = scroll_y;
+ self.content_h = content_h;
+ self.viewport_h = viewport_h;
+ }
+
+ pub fn get_thumb_rect(&self) -> Option<(f32, f32, f32, f32)> {
+ if self.content_h <= self.viewport_h || self.viewport_h <= 0.0 || self.h <= 0.0 {
+ return None;
+ }
+ let sb_x = self.x;
+ let sb_w = self.w;
+ let sb_track_h = self.h;
+ let sb_track_y = self.y;
+
+ let visible_ratio = self.viewport_h / self.content_h;
+ let thumb_h = if sb_track_h <= 20.0 {
+ sb_track_h
+ } else {
+ (sb_track_h * visible_ratio).clamp(20.0, sb_track_h)
+ };
+ let max_scroll = (self.content_h - self.viewport_h).max(0.0);
+ let scroll_ratio = if max_scroll > 0.0 { self.scroll_y / max_scroll } else { 0.0 };
+ let thumb_y = sb_track_y + scroll_ratio * (sb_track_h - thumb_h);
+
+ Some((sb_x, thumb_y, sb_w, thumb_h))
+ }
+}
+
+impl Element for ScrollBar {
+ fn base(&self) -> Option<&Widget> { None }
+ fn base_mut(&mut self) -> Option<&mut Widget> { None }
+
+ fn rect(&self) -> (f32, f32, f32, f32) {
+ (self.x, self.y, self.w, self.h)
+ }
+
+ fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
+ self.x = x;
+ self.y = y;
+ self.w = w;
+ self.h = h;
+ }
+
+ fn as_any(&self) -> &dyn std::any::Any { self }
+ fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
+
+ fn as_ptr(&self) -> *mut (dyn Element + 'static) {
+ self as *const Self as *mut Self as *mut (dyn Element + 'static)
+ }
+
+ fn as_ptr_mut(&mut self) -> *mut (dyn Element + 'static) {
+ self as *mut Self as *mut (dyn Element + 'static)
+ }
+
+ fn color(&self) -> [f32; 4] {
+ [0.0, 0.0, 0.0, 0.0]
+ }
+
+ fn set_hovered(&mut self, hovered: bool) {
+ self.hovered = hovered;
+ }
+
+ fn hovered(&self) -> bool {
+ self.hovered
+ }
+
+ fn hit_test(&self, px: f32, py: f32, ctx: &UiContext) -> bool {
+ if ctx.is_coordinate_covered(self as *const Self as *const () as usize, px, py) {
+ return false;
+ }
+ let hit_margin = 6.0;
+ px >= self.x - hit_margin && px <= self.x + self.w + hit_margin && py >= self.y && py <= self.y + self.h
+ }
+
+ fn on_cursor_moved(&mut self, px: f32, py: f32, ctx: &mut UiContext) -> bool {
+ let mut changed = false;
+ let is_hit = self.hit_test(px, py, ctx);
+ if self.hovered != is_hit {
+ self.hovered = is_hit;
+ changed = true;
+ }
+
+ if self.dragging {
+ if let Some((_, _, _, thumb_h)) = self.get_thumb_rect() {
+ let sb_track_y = self.y;
+ let sb_track_h = self.h;
+ let track_scroll_range = sb_track_h - thumb_h;
+ if track_scroll_range > 0.0 {
+ let mouse_y_in_track = (py - sb_track_y).clamp(0.0, sb_track_h);
+ let scroll_ratio = (mouse_y_in_track - thumb_h / 2.0) / track_scroll_range;
+ let max_scroll = (self.content_h - self.viewport_h).max(0.0);
+ let new_scroll_y = (scroll_ratio.clamp(0.0, 1.0) * max_scroll).clamp(0.0, max_scroll);
+ if (self.scroll_y - new_scroll_y).abs() > 0.01 {
+ self.scroll_y = new_scroll_y;
+ changed = true;
+
+ if let Some(parent_ptr) = self.parent {
+ unsafe {
+ if let Some(page) = (*parent_ptr).as_any_mut().downcast_mut::<Page>() {
+ page.scroll_y = new_scroll_y;
+ let (px, py, pw, ph) = page.rect();
+ page.set_rect(px, py, pw, ph);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ changed
+ }
+
+ fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32, ctx: &mut UiContext) -> bool {
+ if button == MouseButton::Left {
+ match state {
+ ElementState::Pressed => {
+ if self.hit_test(px, py, ctx) {
+ self.dragging = true;
+ self.on_cursor_moved(px, py, ctx);
+ return true;
+ }
+ }
+ ElementState::Released => {
+ if self.dragging {
+ self.dragging = false;
+ return true;
+ }
+ }
+ }
+ }
+ false
+ }
+
+ fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+ let mut quads = Vec::new();
+ if self.content_h > self.viewport_h && self.h > 0.0 {
+ quads.push((self.x, self.y, self.w, self.h, [0.15, 0.15, 0.20, 0.3]));
+
+ if let Some((sb_x, thumb_y, sb_w, thumb_h)) = self.get_thumb_rect() {
+ let thumb_color = if self.dragging {
+ [0.70, 0.70, 0.75, 0.6]
+ } else if self.hovered {
+ [0.65, 0.65, 0.70, 0.5]
+ } else {
+ [0.60, 0.60, 0.65, 0.4]
+ };
+ quads.push((sb_x, thumb_y, sb_w, thumb_h, thumb_color));
+ }
+ }
+ quads
+ }
+
+ fn parent(&self, _ctx: &UiContext) -> Option<*mut (dyn Element + 'static)> {
+ self.parent
+ }
+
+ fn set_parent(&mut self, parent: Option<*mut (dyn Element + 'static)>, _ctx: &mut UiContext) {
+ self.parent = parent;
+ }
+
+ fn children(&self, _ctx: &UiContext) -> Vec<*mut (dyn Element + 'static)> {
+ self.children.clone()
+ }
+
+ fn add_child(&mut self, child: *mut (dyn Element + 'static), _ctx: &mut UiContext) {
+ self.children.push(child);
+ }
+
+ fn clear_children(&mut self, _ctx: &mut UiContext) {
+ self.children.clear();
+ }
+}
+
+impl Drop for ScrollBar {
+ fn drop(&mut self) {
+ clear_widget_references(self);
+ }
+}
+
+unsafe impl Send for ScrollBar {}
+unsafe impl Sync for ScrollBar {}
diff --git a/src/widget/mod.rs b/src/widget/mod.rs
index 95d1d67..b243546 100644
--- a/src/widget/mod.rs
+++ b/src/widget/mod.rs
@@ -573,7 +573,7 @@ pub use self::input::{
pub use self::container::{
Container, Header, ContentBg, ViewportBg, ParametersBg, ScrollingList,
ScrollBox, Menu, MenuBar, Spreadsheet, Breadcrumb, Plate,
- Switcher, Layer, Page, Backplate, Paginator
+ Switcher, Layer, Page, Backplate, Paginator, ScrollBar
};
pub use self::display::{
TextLabel, Label, SectionHeader, StyledLabel, TextItem, Svg, UsageBar,