GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
widget: introduce ContainerLayout strategies and SectionContainer
src/widget/container/container.rs | 34 ++---
src/widget/container/container_layout.rs | 231 ++++++++++++++++++++++++++++++
src/widget/container/mod.rs | 4 +
src/widget/container/page.rs | 56 +++++---
src/widget/container/section_container.rs | 156 ++++++++++++++++++++
src/widget/mod.rs | 3 +-
6 files changed, 442 insertions(+), 42 deletions(-)
diff --git a/src/widget/container/container.rs b/src/widget/container/container.rs
index 7e56ecd..0d6b07e 100644
--- a/src/widget/container/container.rs
+++ b/src/widget/container/container.rs
@@ -1,15 +1,27 @@
use crate::widget::*;
+use super::container_layout::{ContainerLayout, OverlayLayout};
#[derive(Clone)]
pub struct Container {
pub parent: Option<*mut (dyn Element + 'static)>,
pub children: Vec<*mut (dyn Element + 'static)>,
pub base: Widget,
+ pub layout: Box<dyn ContainerLayout>,
}
impl Container {
pub fn new() -> Self {
- Self { parent: None, children: Vec::new(), base: Widget::new() }
+ Self {
+ parent: None,
+ children: Vec::new(),
+ base: Widget::new(),
+ layout: Box::new(OverlayLayout),
+ }
+ }
+
+ pub fn with_layout<L: ContainerLayout + 'static>(mut self, layout: L) -> Self {
+ self.layout = Box::new(layout);
+ self
}
}
@@ -45,29 +57,13 @@ impl Element for Container {
}
fn measure(&self, constraints: LayoutConstraints, ctx: &UiContext) -> Size {
- let mut max_w = 0.0f32;
- let mut max_h = 0.0f32;
- for &child in &self.children {
- unsafe {
- let size = (*child).measure(constraints, ctx);
- max_w = max_w.max(size.width);
- max_h = max_h.max(size.height);
- }
- }
- Size {
- width: max_w.clamp(constraints.min_width, constraints.max_width),
- height: max_h.clamp(constraints.min_height, constraints.max_height),
- }
+ self.layout.measure(constraints, &self.children, ctx)
}
fn layout(&mut self, origin: Point, constraints: LayoutConstraints, ctx: &mut UiContext) {
let size = self.measure(constraints, ctx);
self.set_rect(origin.x, origin.y, size.width, size.height);
- for &child in &self.children {
- unsafe {
- (*child).layout(origin, constraints, ctx);
- }
- }
+ self.layout.layout(origin.x, origin.y, size.width, size.height, &self.children, ctx);
}
fn focus(&mut self) {
diff --git a/src/widget/container/container_layout.rs b/src/widget/container/container_layout.rs
new file mode 100644
index 0000000..d74b7e6
--- /dev/null
+++ b/src/widget/container/container_layout.rs
@@ -0,0 +1,231 @@
+use crate::widget::*;
+use crate::context::UiContext;
+
+pub trait ContainerLayout: std::fmt::Debug {
+ fn layout(&self, x: f32, y: f32, w: f32, h: f32, children: &[*mut (dyn Element + 'static)], ctx: &mut UiContext) -> f32;
+ fn measure(&self, constraints: LayoutConstraints, children: &[*mut (dyn Element + 'static)], ctx: &UiContext) -> Size;
+ fn box_clone(&self) -> Box<dyn ContainerLayout>;
+}
+
+impl Clone for Box<dyn ContainerLayout> {
+ fn clone(&self) -> Self {
+ self.box_clone()
+ }
+}
+
+#[derive(Debug, Clone, Copy)]
+pub struct OverlayLayout;
+
+impl ContainerLayout for OverlayLayout {
+ fn layout(&self, x: f32, y: f32, w: f32, h: f32, children: &[*mut (dyn Element + 'static)], _ctx: &mut UiContext) -> f32 {
+ for &child_ptr in children {
+ unsafe {
+ (*child_ptr).set_rect(x, y, w, h);
+ }
+ }
+ h
+ }
+
+ fn measure(&self, constraints: LayoutConstraints, children: &[*mut (dyn Element + 'static)], ctx: &UiContext) -> Size {
+ let mut max_w = 0.0f32;
+ let mut max_h = 0.0f32;
+ for &child_ptr in children {
+ unsafe {
+ let size = (*child_ptr).measure(constraints, ctx);
+ max_w = max_w.max(size.width);
+ max_h = max_h.max(size.height);
+ }
+ }
+ Size {
+ width: max_w.clamp(constraints.min_width, constraints.max_width),
+ height: max_h.clamp(constraints.min_height, constraints.max_height),
+ }
+ }
+
+ fn box_clone(&self) -> Box<dyn ContainerLayout> {
+ Box::new(*self)
+ }
+}
+
+#[derive(Debug, Clone, Copy)]
+pub struct VerticalLayout {
+ pub padding_x: f32,
+ pub padding_y: f32,
+ pub spacing: f32,
+}
+
+impl Default for VerticalLayout {
+ fn default() -> Self {
+ Self {
+ padding_x: 0.0,
+ padding_y: 0.0,
+ spacing: 8.0,
+ }
+ }
+}
+
+impl ContainerLayout for VerticalLayout {
+ fn layout(&self, x: f32, y: f32, w: f32, _h: f32, children: &[*mut (dyn Element + 'static)], _ctx: &mut UiContext) -> f32 {
+ let left_x = x + self.padding_x;
+ let available_w = (w - 2.0 * self.padding_x).max(1.0);
+ let mut current_y = y + self.padding_y;
+
+ for &child_ptr in children {
+ unsafe {
+ let child = &mut *child_ptr;
+ let ch = child.preferred_height().unwrap_or(child.rect().3);
+ let use_h = if ch > 0.0 { ch } else { 44.0 };
+ child.set_rect(left_x, current_y, available_w, use_h);
+ current_y += use_h + self.spacing;
+ }
+ }
+ (current_y - y).max(0.0)
+ }
+
+ fn measure(&self, constraints: LayoutConstraints, children: &[*mut (dyn Element + 'static)], ctx: &UiContext) -> Size {
+ let mut total_h = self.padding_y * 2.0;
+ let mut max_w = 0.0f32;
+ let spacing = self.spacing;
+
+ for (i, &child_ptr) in children.iter().enumerate() {
+ unsafe {
+ let size = (*child_ptr).measure(constraints, ctx);
+ max_w = max_w.max(size.width);
+ total_h += size.height;
+ if i > 0 {
+ total_h += spacing;
+ }
+ }
+ }
+
+ Size {
+ width: (max_w + self.padding_x * 2.0).clamp(constraints.min_width, constraints.max_width),
+ height: total_h.clamp(constraints.min_height, constraints.max_height),
+ }
+ }
+
+ fn box_clone(&self) -> Box<dyn ContainerLayout> {
+ Box::new(*self)
+ }
+}
+
+#[derive(Debug, Clone, Copy)]
+pub struct GridLayout {
+ pub columns: usize,
+ pub gap: f32,
+ pub padding_x: f32,
+ pub padding_y: f32,
+}
+
+impl ContainerLayout for GridLayout {
+ fn layout(&self, x: f32, y: f32, w: f32, _h: f32, children: &[*mut (dyn Element + 'static)], _ctx: &mut UiContext) -> f32 {
+ let count = children.len();
+ if count == 0 {
+ return 0.0;
+ }
+ let cols = self.columns.max(1);
+ let total_gap = self.gap * (cols - 1) as f32;
+ let available_w = (w - 2.0 * self.padding_x - total_gap).max(1.0);
+ let col_w = available_w / cols as f32;
+
+ let mut col_heights = vec![y + self.padding_y; cols];
+
+ for &child_ptr in children {
+ unsafe {
+ let child = &mut *child_ptr;
+ let ch = child.preferred_height().unwrap_or(child.rect().3);
+ let use_h = if ch > 0.0 { ch } else { 44.0 };
+
+ let mut min_col = 0;
+ let mut min_h = col_heights[0];
+ for i in 1..cols {
+ if col_heights[i] < min_h {
+ min_h = col_heights[i];
+ min_col = i;
+ }
+ }
+
+ let cx = x + self.padding_x + min_col as f32 * (col_w + self.gap);
+ let cy = col_heights[min_col];
+ child.set_rect(cx, cy, col_w, use_h);
+ col_heights[min_col] += use_h + self.gap;
+ }
+ }
+
+ let max_h = col_heights.iter().cloned().fold(0.0f32, |a, b| a.max(b));
+ (max_h - y).max(0.0)
+ }
+
+ fn measure(&self, constraints: LayoutConstraints, children: &[*mut (dyn Element + 'static)], ctx: &UiContext) -> Size {
+ let cols = self.columns.max(1);
+ let mut col_heights = vec![self.padding_y; cols];
+ let total_gap = self.gap * (cols - 1) as f32;
+ let available_w = (constraints.max_width - 2.0 * self.padding_x - total_gap).max(1.0);
+ let col_w = available_w / cols as f32;
+
+ let child_constraints = LayoutConstraints::new(col_w, col_w, constraints.min_height, constraints.max_height);
+
+ for &child_ptr in children {
+ unsafe {
+ let size = (*child_ptr).measure(child_constraints, ctx);
+ let mut min_col = 0;
+ let mut min_h = col_heights[0];
+ for i in 1..cols {
+ if col_heights[i] < min_h {
+ min_h = col_heights[i];
+ min_col = i;
+ }
+ }
+ col_heights[min_col] += size.height + self.gap;
+ }
+ }
+
+ let max_h = col_heights.iter().cloned().fold(0.0f32, |a, b| a.max(b));
+ Size {
+ width: constraints.max_width,
+ height: (max_h + self.padding_y).clamp(constraints.min_height, constraints.max_height),
+ }
+ }
+
+ fn box_clone(&self) -> Box<dyn ContainerLayout> {
+ Box::new(*self)
+ }
+}
+
+#[derive(Debug, Clone, Copy)]
+pub struct AdaptiveGridLayout {
+ pub min_col_width: f32,
+ pub gap: f32,
+ pub padding_x: f32,
+ pub padding_y: f32,
+}
+
+impl ContainerLayout for AdaptiveGridLayout {
+ fn layout(&self, x: f32, y: f32, w: f32, h: f32, children: &[*mut (dyn Element + 'static)], ctx: &mut UiContext) -> f32 {
+ let usable_w = (w - 2.0 * self.padding_x).max(1.0);
+ let cols = (((usable_w + self.gap) / (self.min_col_width + self.gap)).floor().max(1.0)) as usize;
+ let grid = GridLayout {
+ columns: cols,
+ gap: self.gap,
+ padding_x: self.padding_x,
+ padding_y: self.padding_y,
+ };
+ grid.layout(x, y, w, h, children, ctx)
+ }
+
+ fn measure(&self, constraints: LayoutConstraints, children: &[*mut (dyn Element + 'static)], ctx: &UiContext) -> Size {
+ let usable_w = (constraints.max_width - 2.0 * self.padding_x).max(1.0);
+ let cols = (((usable_w + self.gap) / (self.min_col_width + self.gap)).floor().max(1.0)) as usize;
+ let grid = GridLayout {
+ columns: cols,
+ gap: self.gap,
+ padding_x: self.padding_x,
+ padding_y: self.padding_y,
+ };
+ grid.measure(constraints, children, ctx)
+ }
+
+ fn box_clone(&self) -> Box<dyn ContainerLayout> {
+ Box::new(*self)
+ }
+}
diff --git a/src/widget/container/mod.rs b/src/widget/container/mod.rs
index 53fbb89..c6a7449 100644
--- a/src/widget/container/mod.rs
+++ b/src/widget/container/mod.rs
@@ -1,4 +1,6 @@
pub mod container;
+pub mod container_layout;
+pub mod section_container;
pub mod header;
pub mod content_bg;
pub mod viewport_bg;
@@ -17,6 +19,8 @@ pub mod paginator;
pub mod scroll_bar;
pub use container::Container;
+pub use container_layout::{ContainerLayout, OverlayLayout, VerticalLayout, GridLayout, AdaptiveGridLayout};
+pub use section_container::SectionContainer;
pub use header::Header;
pub use content_bg::ContentBg;
pub use viewport_bg::ViewportBg;
diff --git a/src/widget/container/page.rs b/src/widget/container/page.rs
index d57da3e..d8c0ca0 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)]) -> f32;
+ fn layout(&self, x: f32, y: f32, w: f32, h: f32, children: &[*mut (dyn Element + 'static)], ctx: &mut UiContext) -> 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)]) -> f32 {
+ fn layout(&self, x: f32, y: f32, w: f32, _h: f32, children: &[*mut (dyn Element + 'static)], ctx: &mut UiContext) -> f32 {
let padding_x = self.padding_x;
let padding_y = self.padding_y;
let left_x = x + padding_x;
@@ -37,7 +37,11 @@ impl PageLayout for VerticalLayout {
let child = unsafe { &mut *child_ptr };
let (_, _, _, ch) = child.rect();
let use_h = if ch > 0.0 { ch } else { 42.0 };
- child.set_rect(left_x, current_y, available_w, use_h);
+ child.layout(
+ Point { x: left_x, y: current_y },
+ LayoutConstraints::new(available_w, available_w, use_h, use_h),
+ ctx,
+ );
current_y += use_h + spacing;
}
current_y - y
@@ -62,7 +66,7 @@ impl Default for ColumnsLayout {
}
impl PageLayout for ColumnsLayout {
- fn layout(&self, x: f32, y: f32, w: f32, h: f32, children: &[*mut (dyn Element + 'static)]) -> f32 {
+ fn layout(&self, x: f32, y: f32, w: f32, h: f32, children: &[*mut (dyn Element + 'static)], ctx: &mut UiContext) -> f32 {
let count = children.len();
if count == 0 {
return 0.0;
@@ -77,7 +81,11 @@ impl PageLayout for ColumnsLayout {
let mut current_x = x + self.padding_x;
for &child_ptr in children {
let child = unsafe { &mut *child_ptr };
- child.set_rect(current_x, start_y, col_w, use_h);
+ child.layout(
+ Point { x: current_x, y: start_y },
+ LayoutConstraints::new(col_w, col_w, use_h, use_h),
+ ctx,
+ );
current_x += col_w + self.spacing;
}
use_h + 2.0 * self.padding_y
@@ -178,24 +186,28 @@ impl Element for Page {
fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
self.base.set_rect(x, y, w, h);
- if !self.visible {
- return;
- }
- let layout_content_h = self.layout.layout(x, y, w, h, &self.base.children);
- self.content_h = self.content_h.max(layout_content_h);
-
- let max_scroll = (self.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, self.content_h, h);
+ }
+
+ fn layout(&mut self, origin: Point, constraints: LayoutConstraints, ctx: &mut UiContext) {
+ let size = self.measure(constraints, ctx);
+ self.set_rect(origin.x, origin.y, size.width, size.height);
- let self_ptr = self as *mut Page as *mut (dyn Element + 'static);
- self.scroll_bar.parent = Some(self_ptr);
+ if self.visible {
+ let layout_content_h = self.layout.layout(origin.x, origin.y, size.width, size.height, &self.base.children, ctx);
+ self.content_h = self.content_h.max(layout_content_h);
+
+ let max_scroll = (self.content_h - size.height).max(0.0);
+ self.scroll_y = self.scroll_y.clamp(0.0, max_scroll);
+
+ let sb_w = 6.0;
+ let sb_padding = 2.0;
+ let sb_x = origin.x + size.width - sb_w - sb_padding;
+ self.scroll_bar.set_rect(sb_x, origin.y + 4.0, sb_w, size.height - 8.0);
+ self.scroll_bar.update(self.scroll_y, self.content_h, size.height);
+
+ let self_ptr = self as *mut Page as *mut (dyn Element + 'static);
+ self.scroll_bar.parent = Some(self_ptr);
+ }
}
fn color(&self) -> [f32; 4] {
diff --git a/src/widget/container/section_container.rs b/src/widget/container/section_container.rs
new file mode 100644
index 0000000..7f2df81
--- /dev/null
+++ b/src/widget/container/section_container.rs
@@ -0,0 +1,156 @@
+use crate::widget::*;
+use crate::context::UiContext;
+use crate::widget::display::TextLabel;
+use super::container::Container;
+use super::container_layout::ContainerLayout;
+
+#[derive(Clone)]
+pub struct SectionContainer {
+ pub header: SectionHeader,
+ pub container: Container,
+ pub base: Widget,
+ pub parent: Option<*mut (dyn Element + 'static)>,
+}
+
+impl SectionContainer {
+ pub fn new(title: &str) -> Self {
+ Self {
+ header: SectionHeader::new(title),
+ container: Container::new(),
+ base: Widget::new(),
+ parent: None,
+ }
+ }
+
+ pub fn with_layout<L: ContainerLayout + 'static>(mut self, layout: L) -> Self {
+ let container = std::mem::replace(&mut self.container, Container::new());
+ self.container = container.with_layout(layout);
+ self
+ }
+}
+
+impl Default for SectionContainer {
+ fn default() -> Self {
+ Self::new("")
+ }
+}
+
+impl Element for SectionContainer {
+ fn base(&self) -> Option<&Widget> { Some(&self.base) }
+ fn blocks_backplate_drag(&self) -> bool { false }
+ fn base_mut(&mut self) -> Option<&mut Widget> { Some(&mut self.base) }
+ fn color(&self) -> [f32; 4] { [0.0, 0.0, 0.0, 0.0] }
+ 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 set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
+ self.base.x = x;
+ self.base.y = y;
+ self.base.w = w;
+ self.base.h = h;
+
+ self.header.set_rect(x, y, w, 24.0);
+ self.container.set_rect(x, y + 28.0, w, (h - 28.0).max(0.0));
+ }
+
+ fn measure(&self, constraints: LayoutConstraints, ctx: &UiContext) -> Size {
+ let remaining_constraints = LayoutConstraints::new(
+ constraints.min_width,
+ constraints.max_width,
+ (constraints.min_height - 28.0).max(0.0),
+ (constraints.max_height - 28.0).max(0.0),
+ );
+ let container_size = self.container.measure(remaining_constraints, ctx);
+ Size {
+ width: container_size.width,
+ height: container_size.height + 28.0,
+ }
+ }
+
+ fn layout(&mut self, origin: Point, constraints: LayoutConstraints, ctx: &mut UiContext) {
+ let size = self.measure(constraints, ctx);
+ self.set_rect(origin.x, origin.y, size.width, size.height);
+
+ let self_ptr = self.as_ptr_mut();
+ self.header.set_parent(Some(self_ptr), ctx);
+ self.container.set_parent(Some(self_ptr), ctx);
+
+ self.header.layout(origin, LayoutConstraints::new(size.width, size.width, 24.0, 24.0), ctx);
+ self.container.layout(
+ Point { x: origin.x, y: origin.y + 28.0 },
+ LayoutConstraints::new(size.width, size.width, (size.height - 28.0).max(0.0), (size.height - 28.0).max(0.0)),
+ ctx,
+ );
+ }
+
+ 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)> {
+ vec![
+ &self.header as *const SectionHeader as *mut SectionHeader as *mut (dyn Element + 'static),
+ &self.container as *const Container as *mut Container as *mut (dyn Element + 'static),
+ ]
+ }
+
+ fn add_child(&mut self, child: *mut (dyn Element + 'static), ctx: &mut UiContext) {
+ self.container.add_child(child, ctx);
+ let id = self.base.id();
+ unsafe {
+ let c_id = (*child).base().map(|b| b.id()).unwrap();
+ ctx.register_widget(c_id, child);
+ ctx.link_ids(id, c_id);
+ (*child).set_parent(Some(self.container.as_ptr_mut()), ctx);
+ }
+ }
+
+ fn clear_children(&mut self, ctx: &mut UiContext) {
+ self.container.clear_children(ctx);
+ ctx.clear_children_ids(self.base.id());
+ }
+
+ fn all_quads(&self, ctx: &UiContext) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+ let mut quads = self.header.all_quads(ctx);
+ quads.extend(self.header.extra_quads());
+ quads.extend(self.container.all_quads(ctx));
+ quads
+ }
+
+ fn text_labels(&self) -> Vec<TextLabel> {
+ let mut labels = self.header.text_labels();
+ labels.extend(self.container.text_labels());
+ labels
+ }
+
+ fn text_labels_with_bounds(&self, ctx: &UiContext) -> Vec<(TextLabel, Option<[f32; 4]>)> {
+ let mut result = self.header.text_labels_with_bounds(ctx);
+ result.extend(self.container.text_labels_with_bounds(ctx));
+ result
+ }
+
+ fn text_labels_with_font_and_bounds(&self, ctx: &UiContext) -> Vec<(TextLabel, Option<String>, Option<[f32; 4]>)> {
+ let mut result = self.header.text_labels_with_font_and_bounds(ctx);
+ result.extend(self.container.text_labels_with_font_and_bounds(ctx));
+ result
+ }
+
+ fn get_text_items(&self) -> Vec<(&glyphon::Buffer, f32, f32, glyphon::Color)> {
+ let mut result = self.header.get_text_items();
+ result.extend(self.container.get_text_items());
+ result
+ }
+
+ fn hit_test(&self, px: f32, py: f32, ctx: &UiContext) -> bool {
+ self.header.hit_test(px, py, ctx) || self.container.hit_test(px, py, ctx)
+ }
+}
+
+impl Drop for SectionContainer {
+ fn drop(&mut self) {
+ clear_widget_references(self);
+ }
+}
diff --git a/src/widget/mod.rs b/src/widget/mod.rs
index cda76b9..46a768e 100644
--- a/src/widget/mod.rs
+++ b/src/widget/mod.rs
@@ -572,7 +572,8 @@ pub use self::input::{
KeybindsControl, KeybindRow
};
pub use self::container::{
- Container, Header, ContentBg, ViewportBg, ParametersBg, ScrollingList,
+ Container, ContainerLayout, OverlayLayout, VerticalLayout, GridLayout, AdaptiveGridLayout,
+ SectionContainer, Header, ContentBg, ViewportBg, ParametersBg, ScrollingList,
ScrollBox, Menu, MenuBar, Spreadsheet, Breadcrumb, Plate,
Switcher, Layer, Page, Backplate, Paginator, ScrollBar
};