GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
Fix backplate drag interfering with pointer events on scrolled widgets and apply UI updates
src/context.rs | 174 +++++++++++++++--
src/layout.rs | 317 ++++++++++++++++++++++++++++++-
src/main.rs | 71 +++++--
src/widget/container/container.rs | 2 +-
src/widget/container/container_layout.rs | 161 ++++++++++++++--
src/widget/container/paginator.rs | 12 ++
src/widget/container/spreadsheet.rs | 4 +
src/widget/container/switcher.rs | 11 ++
src/widget/input/button_strip.rs | 4 +
src/widget/input/color_selector.rs | 4 +
src/widget/json_layout.rs | 4 +
src/widget/mod.rs | 31 ++-
12 files changed, 737 insertions(+), 58 deletions(-)
diff --git a/src/context.rs b/src/context.rs
index 7a1a5f8..df52778 100644
--- a/src/context.rs
+++ b/src/context.rs
@@ -3,6 +3,52 @@ use crate::widget::{Element, WidgetId, LayoutTree, Key, MouseButton, ElementStat
use crate::widget::core::hover_animation::HoverState;
use crate::widget::core::context_menu::ContextMenuState;
+pub struct SpatialGrid {
+ pub cell_size: f32,
+ pub cells: HashMap<(i32, i32), Vec<WidgetId>>,
+}
+
+impl SpatialGrid {
+ pub fn new(cell_size: f32) -> Self {
+ Self {
+ cell_size,
+ cells: HashMap::new(),
+ }
+ }
+
+ pub fn clear(&mut self) {
+ self.cells.clear();
+ }
+
+ pub fn insert(&mut self, id: WidgetId, rect: (f32, f32, f32, f32)) {
+ let (x, y, w, h) = rect;
+ if w <= 0.0 || h <= 0.0 {
+ return;
+ }
+ let start_x = (x / self.cell_size).floor() as i32;
+ let end_x = ((x + w) / self.cell_size).floor() as i32;
+ let start_y = (y / self.cell_size).floor() as i32;
+ let end_y = ((y + h) / self.cell_size).floor() as i32;
+
+ let start_x = start_x.max(-1000);
+ let end_x = end_x.min(1000);
+ let start_y = start_y.max(-1000);
+ let end_y = end_y.min(1000);
+
+ for cx in start_x..=end_x {
+ for cy in start_y..=end_y {
+ self.cells.entry((cx, cy)).or_default().push(id);
+ }
+ }
+ }
+
+ pub fn query(&self, px: f32, py: f32) -> &[WidgetId] {
+ let cx = (px / self.cell_size).floor() as i32;
+ let cy = (py / self.cell_size).floor() as i32;
+ self.cells.get(&(cx, cy)).map(|v| v.as_slice()).unwrap_or(&[])
+ }
+}
+
pub struct UiContext {
pub layout_tree: LayoutTree,
pub widget_registry: HashMap<WidgetId, *mut (dyn Element + 'static)>,
@@ -15,6 +61,9 @@ pub struct UiContext {
pub drag_start_pos: Option<(f32, f32)>,
pub drag_target: Option<WidgetId>,
pub is_dragging: bool,
+ pub any_dirty: bool,
+ pub tick_receivers: Vec<WidgetId>,
+ pub spatial_grid: SpatialGrid,
}
impl UiContext {
@@ -34,6 +83,9 @@ impl UiContext {
drag_start_pos: None,
drag_target: None,
is_dragging: false,
+ any_dirty: false,
+ tick_receivers: Vec::new(),
+ spatial_grid: SpatialGrid::new(100.0),
}
}
@@ -49,6 +101,9 @@ impl UiContext {
if root.is_null() {
return false;
}
+ if let Event::Tick(_) = event {
+ return false;
+ }
unsafe {
// Track drag gestures based on mouse events
match event {
@@ -218,28 +273,89 @@ impl UiContext {
}
pub fn is_dirty(&self) -> bool {
+ self.any_dirty
+ }
+
+ pub fn clear_dirty(&mut self) {
+ self.any_dirty = false;
for &ptr in self.widget_registry.values() {
unsafe {
- if let Some(b) = (*ptr).base() {
- if b.dirty {
- return true;
- }
+ if let Some(b) = (*ptr).base_mut() {
+ b.dirty = false;
}
}
}
- false
+ self.rebuild_spatial_grid();
}
- pub fn clear_dirty(&mut self) {
- for &ptr in self.widget_registry.values() {
+ pub fn rebuild_spatial_grid(&mut self) {
+ self.spatial_grid.clear();
+ for (&id, &ptr) in &self.widget_registry {
unsafe {
- if let Some(b) = (*ptr).base_mut() {
- b.dirty = false;
+ if !ptr.is_null() {
+ let rect = (*ptr).rect();
+ self.spatial_grid.insert(id, rect);
}
}
}
}
+ pub fn register_tick_receiver(&mut self, id: WidgetId) {
+ if !self.tick_receivers.contains(&id) {
+ self.tick_receivers.push(id);
+ }
+ }
+
+ pub fn unregister_tick_receiver(&mut self, id: WidgetId) {
+ self.tick_receivers.retain(|&x| x != id);
+ }
+
+ pub fn is_widget_visible(&self, id: WidgetId) -> bool {
+ let mut curr = id;
+ loop {
+ if let Some(w_ptr) = self.widget_registry.get(&curr) {
+ unsafe {
+ if !(*(*w_ptr)).visible() {
+ return false;
+ }
+ }
+ } else {
+ return false;
+ }
+ if let Some(&parent_id) = self.layout_tree.parents.get(&curr) {
+ if let Some(parent_ptr) = self.widget_registry.get(&parent_id) {
+ unsafe {
+ if !(*(*parent_ptr)).is_child_visible(curr) {
+ return false;
+ }
+ }
+ }
+ curr = parent_id;
+ } else {
+ break;
+ }
+ }
+ true
+ }
+
+ pub fn tick(&mut self, dt: f32) -> bool {
+ let mut changed = false;
+ let ids = self.tick_receivers.clone();
+ for id in ids {
+ if self.is_widget_visible(id) {
+ if let Some(ptr) = self.widget_registry.get(&id).copied() {
+ unsafe {
+ if (*ptr).tick(dt, self) {
+ (*ptr).mark_dirty(self);
+ changed = true;
+ }
+ }
+ }
+ }
+ }
+ changed
+ }
+
// --- Focus management ---
pub fn set_focused(&mut self, w: &mut dyn Element) {
let new_ptr = unsafe {
@@ -375,6 +491,11 @@ impl UiContext {
// --- Registry ---
pub fn register_widget(&mut self, id: WidgetId, ptr: *mut (dyn Element + 'static)) {
self.widget_registry.insert(id, ptr);
+ unsafe {
+ if !ptr.is_null() && (*ptr).wants_tick() {
+ self.register_tick_receiver(id);
+ }
+ }
}
pub fn link_ids(&mut self, parent: WidgetId, child: WidgetId) {
@@ -404,6 +525,7 @@ impl UiContext {
self.layout_tree.parents.clear();
self.layout_tree.children.clear();
self.widget_registry.clear();
+ self.tick_receivers.clear();
}
// --- Popovers ---
@@ -630,17 +752,31 @@ impl UiContext {
pub fn is_movable_backplate_at(&self, px: f32, py: f32) -> bool {
let mut hit_backplate = false;
- for &ptr in self.widget_registry.values() {
- unsafe {
- if !ptr.is_null() {
- let w = &*ptr;
- if w.hit_test(px, py, self) {
- if w.is_backplate() {
- if w.is_movable_backplate() {
- hit_backplate = true;
+ let scroll_y = self.get_scroll_offset();
+ let mut candidate_ids = self.spatial_grid.query(px, py).to_vec();
+ if scroll_y != 0.0 {
+ candidate_ids.extend_from_slice(self.spatial_grid.query(px, py + scroll_y));
+ candidate_ids.sort_unstable();
+ candidate_ids.dedup();
+ }
+ for &id in &candidate_ids {
+ if let Some(&ptr) = self.widget_registry.get(&id) {
+ unsafe {
+ if !ptr.is_null() {
+ let w = &*ptr;
+ let is_hit = if w.is_backplate() {
+ w.hit_test(px, py, self)
+ } else {
+ w.hit_test(px, py, self) || (scroll_y != 0.0 && w.hit_test(px, py + scroll_y, self))
+ };
+ if is_hit {
+ if w.is_backplate() {
+ if w.is_movable_backplate() {
+ hit_backplate = true;
+ }
+ } else if w.blocks_backplate_drag() {
+ return false;
}
- } else if w.blocks_backplate_drag() {
- return false;
}
}
}
diff --git a/src/layout.rs b/src/layout.rs
index aab4b41..f31f25d 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -2358,6 +2358,7 @@ impl SplitterLayout {
}
}
+#[derive(Debug, Clone)]
pub struct Grid {
pub left: f32,
pub top: f32,
@@ -2458,6 +2459,7 @@ impl CircularPaneLayout {
}
}
+#[derive(Debug, Clone)]
pub struct Radial {
pub center_x: f32,
pub center_y: f32,
@@ -2529,12 +2531,45 @@ impl Radial {
}
}
-pub trait LayoutStrategy {
+use std::cell::RefCell;
+use std::collections::HashMap;
+
+thread_local! {
+ pub static GRID_STATES: RefCell<HashMap<usize, Grid>> = RefCell::new(HashMap::new());
+ pub static OVERLAY_STATES: RefCell<HashMap<usize, (f32, f32, f32, f32)>> = RefCell::new(HashMap::new());
+ pub static VERTICAL_STATES: RefCell<HashMap<usize, (f32, f32)>> = RefCell::new(HashMap::new());
+}
+
+pub fn save_grid_state(ptr: usize, grid: Grid) {
+ GRID_STATES.with(|m| m.borrow_mut().insert(ptr, grid));
+}
+
+pub fn mutate_grid_state<F, R>(ptr: usize, mut f: F) -> Option<R>
+where
+ F: FnMut(&mut Grid) -> R,
+{
+ GRID_STATES.with(|m| {
+ let mut map = m.borrow_mut();
+ map.get_mut(&ptr).map(|grid| f(grid))
+ })
+}
+
+pub trait LayoutStrategy: std::fmt::Debug {
fn init(&mut self, left: f32, top: f32, width: f32, height: f32);
fn allocate(&mut self, ww: f32, wh: f32) -> (f32, f32, f32, f32);
fn set_section_count(&mut self, _count: usize) {}
fn get_column_width(&self) -> Option<f32> { None }
fn get_gap(&self) -> f32 { 20.0 }
+
+ fn layout(&self, x: f32, y: f32, w: f32, h: f32, children: &[*mut (dyn crate::widget::Element + 'static)], ctx: &mut crate::context::UiContext) -> f32;
+ fn measure(&self, constraints: crate::widget::LayoutConstraints, children: &[*mut (dyn crate::widget::Element + 'static)], ctx: &crate::context::UiContext) -> crate::widget::Size;
+ fn box_clone(&self) -> Box<dyn LayoutStrategy>;
+}
+
+impl Clone for Box<dyn LayoutStrategy> {
+ fn clone(&self) -> Self {
+ self.box_clone()
+ }
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -2543,6 +2578,7 @@ pub enum FlexDirection {
Column,
}
+#[derive(Debug, Clone)]
pub struct FlexLayout {
left: f32,
top: f32,
@@ -2599,8 +2635,86 @@ impl LayoutStrategy for FlexLayout {
fn get_gap(&self) -> f32 {
self.spacing
}
+
+ fn layout(&self, x: f32, y: f32, w: f32, h: f32, children: &[*mut (dyn crate::widget::Element + 'static)], _ctx: &mut crate::context::UiContext) -> f32 {
+ let mut cur_x = x;
+ let mut cur_y = y;
+ match self.direction {
+ FlexDirection::Row => {
+ for &child_ptr in children {
+ unsafe {
+ let child = &mut *child_ptr;
+ let child_w = child.rect().2;
+ let child_h = child.preferred_height().unwrap_or(child.rect().3);
+ let use_h = if child_h > 0.0 { child_h } else { h };
+ child.set_rect(cur_x, cur_y, child_w, use_h);
+ cur_x += child_w + self.spacing;
+ }
+ }
+ (cur_x - x).max(0.0)
+ }
+ FlexDirection::Column => {
+ for &child_ptr in children {
+ unsafe {
+ let child = &mut *child_ptr;
+ let child_h = child.preferred_height().unwrap_or(child.rect().3);
+ let use_h = if child_h > 0.0 { child_h } else { 44.0 };
+ child.set_rect(x, cur_y, w, use_h);
+ cur_y += use_h + self.spacing;
+ }
+ }
+ (cur_y - y).max(0.0)
+ }
+ }
+ }
+
+ fn measure(&self, constraints: crate::widget::LayoutConstraints, children: &[*mut (dyn crate::widget::Element + 'static)], ctx: &crate::context::UiContext) -> crate::widget::Size {
+ match self.direction {
+ FlexDirection::Row => {
+ let mut total_w = 0.0f32;
+ let mut max_h = 0.0f32;
+ for (i, &child_ptr) in children.iter().enumerate() {
+ unsafe {
+ let size = (*child_ptr).measure(constraints, ctx);
+ total_w += size.width;
+ max_h = max_h.max(size.height);
+ if i > 0 {
+ total_w += self.spacing;
+ }
+ }
+ }
+ crate::widget::Size {
+ width: total_w.clamp(constraints.min_width, constraints.max_width),
+ height: max_h.clamp(constraints.min_height, constraints.max_height),
+ }
+ }
+ FlexDirection::Column => {
+ let mut total_h = 0.0f32;
+ let mut max_w = 0.0f32;
+ for (i, &child_ptr) in children.iter().enumerate() {
+ unsafe {
+ let size = (*child_ptr).measure(constraints, ctx);
+ total_h += size.height;
+ max_w = max_w.max(size.width);
+ if i > 0 {
+ total_h += self.spacing;
+ }
+ }
+ }
+ crate::widget::Size {
+ width: max_w.clamp(constraints.min_width, constraints.max_width),
+ height: total_h.clamp(constraints.min_height, constraints.max_height),
+ }
+ }
+ }
+ }
+
+ fn box_clone(&self) -> Box<dyn LayoutStrategy> {
+ Box::new(self.clone())
+ }
}
+#[derive(Debug, Clone)]
pub struct ColumnLayout {
left: f32,
top: f32,
@@ -2643,8 +2757,46 @@ impl LayoutStrategy for ColumnLayout {
fn get_gap(&self) -> f32 {
self.gap
}
+
+ fn layout(&self, x: f32, y: f32, w: f32, _h: f32, children: &[*mut (dyn crate::widget::Element + 'static)], _ctx: &mut crate::context::UiContext) -> f32 {
+ let mut cur_y = y;
+ for &child_ptr in children {
+ unsafe {
+ let child = &mut *child_ptr;
+ let child_h = child.preferred_height().unwrap_or(child.rect().3);
+ let use_h = if child_h > 0.0 { child_h } else { 44.0 };
+ child.set_rect(x, cur_y, w, use_h);
+ cur_y += use_h + self.gap;
+ }
+ }
+ (cur_y - y).max(0.0)
+ }
+
+ fn measure(&self, constraints: crate::widget::LayoutConstraints, children: &[*mut (dyn crate::widget::Element + 'static)], ctx: &crate::context::UiContext) -> crate::widget::Size {
+ let mut total_h = 0.0f32;
+ let mut max_w = 0.0f32;
+ for (i, &child_ptr) in children.iter().enumerate() {
+ unsafe {
+ let size = (*child_ptr).measure(constraints, ctx);
+ total_h += size.height;
+ max_w = max_w.max(size.width);
+ if i > 0 {
+ total_h += self.gap;
+ }
+ }
+ }
+ crate::widget::Size {
+ width: max_w.clamp(constraints.min_width, constraints.max_width),
+ height: total_h.clamp(constraints.min_height, constraints.max_height),
+ }
+ }
+
+ fn box_clone(&self) -> Box<dyn LayoutStrategy> {
+ Box::new(self.clone())
+ }
}
+#[derive(Debug, Clone)]
pub struct AdaptiveGrid {
grid: Option<Grid>,
#[allow(dead_code)]
@@ -2737,8 +2889,94 @@ impl LayoutStrategy for AdaptiveGrid {
fn get_gap(&self) -> f32 {
self.gap
}
+
+ fn layout(&self, x: f32, y: f32, w: f32, _h: f32, children: &[*mut (dyn crate::widget::Element + 'static)], _ctx: &mut crate::context::UiContext) -> f32 {
+ let usable_w = w.max(1.0);
+ let min_col_width = crate::layout::grid_min_col_width();
+ let cols = (((usable_w + self.gap) / (min_col_width + self.gap)).floor().max(1.0)) as usize;
+ let count = if let Some(n) = self.num_sections {
+ n.min(cols).max(1)
+ } else {
+ cols
+ };
+
+ let total_gap = self.gap * (count - 1) as f32;
+ let available_w = (w - total_gap).max(1.0);
+ let col_w = available_w / count as f32;
+
+ let mut col_heights = vec![y; count];
+
+ 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..count {
+ if col_heights[i] < min_h {
+ min_h = col_heights[i];
+ min_col = i;
+ }
+ }
+
+ let cx = 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: crate::widget::LayoutConstraints, children: &[*mut (dyn crate::widget::Element + 'static)], ctx: &crate::context::UiContext) -> crate::widget::Size {
+ let usable_w = constraints.max_width.max(1.0);
+ let min_col_width = crate::layout::grid_min_col_width();
+ let cols = (((usable_w + self.gap) / (min_col_width + self.gap)).floor().max(1.0)) as usize;
+ let count = if let Some(n) = self.num_sections {
+ n.min(cols).max(1)
+ } else {
+ cols
+ };
+
+ let mut col_heights = vec![0.0f32; count];
+ let total_gap = self.gap * (count - 1) as f32;
+ let available_w = (constraints.max_width - total_gap).max(1.0);
+ let col_w = available_w / count as f32;
+
+ let child_constraints = crate::widget::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..count {
+ 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));
+ crate::widget::Size {
+ width: constraints.max_width,
+ height: max_h.clamp(constraints.min_height, constraints.max_height),
+ }
+ }
+
+ fn box_clone(&self) -> Box<dyn LayoutStrategy> {
+ Box::new(self.clone())
+ }
}
+#[derive(Debug, Clone)]
pub struct RadialLayout {
radial: Option<Radial>,
aspect_ratio: f32,
@@ -2780,6 +3018,59 @@ impl LayoutStrategy for RadialLayout {
(0.0, 0.0, ww, wh)
}
}
+
+ fn layout(&self, x: f32, y: f32, w: f32, h: f32, children: &[*mut (dyn crate::widget::Element + 'static)], _ctx: &mut crate::context::UiContext) -> f32 {
+ let cx = x + w / 2.0;
+ let cy = y + h / 2.0;
+ let aspect = if self.aspect_ratio > 0.0 {
+ self.aspect_ratio
+ } else {
+ let screen_aspect = (w / h.max(1.0)).max(0.1);
+ 1.0 + (screen_aspect - 1.0) * 0.4
+ };
+ let radial = Radial::new(cx, cy, aspect, self.base_spacing);
+ for (idx, &child_ptr) in children.iter().enumerate() {
+ unsafe {
+ let child = &mut *child_ptr;
+ let cw = child.rect().2;
+ let ch = child.preferred_height().unwrap_or(child.rect().3);
+ let use_h = if ch > 0.0 { ch } else { 44.0 };
+ let (rx, ry, rw, rh) = radial.widget_rect(idx, cw, use_h);
+ child.set_rect(rx, ry, rw, rh);
+ }
+ }
+ h
+ }
+
+ fn measure(&self, constraints: crate::widget::LayoutConstraints, children: &[*mut (dyn crate::widget::Element + 'static)], ctx: &crate::context::UiContext) -> crate::widget::Size {
+ let cx = constraints.max_width / 2.0;
+ let cy = constraints.max_height / 2.0;
+ let aspect = if self.aspect_ratio > 0.0 {
+ self.aspect_ratio
+ } else {
+ let screen_aspect = (constraints.max_width / constraints.max_height.max(1.0)).max(0.1);
+ 1.0 + (screen_aspect - 1.0) * 0.4
+ };
+ let radial = Radial::new(cx, cy, aspect, self.base_spacing);
+ let mut max_w = 0.0f32;
+ let mut max_h = 0.0f32;
+ for (idx, &child_ptr) in children.iter().enumerate() {
+ unsafe {
+ let size = (*child_ptr).measure(constraints, ctx);
+ let (rx, ry, rw, rh) = radial.widget_rect(idx, size.width, size.height);
+ max_w = max_w.max(rx + rw);
+ max_h = max_h.max(ry + rh);
+ }
+ }
+ crate::widget::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 LayoutStrategy> {
+ Box::new(self.clone())
+ }
}
pub struct PageLayoutBuilder<'a, P> {
@@ -3230,6 +3521,30 @@ pub fn get_system_monospace_font() -> &'static str {
})
}
+impl crate::widget::ContainerLayout for FlexLayout {
+ fn box_clone_container(&self) -> Box<dyn crate::widget::ContainerLayout> {
+ Box::new(self.clone())
+ }
+}
+
+impl crate::widget::ContainerLayout for ColumnLayout {
+ fn box_clone_container(&self) -> Box<dyn crate::widget::ContainerLayout> {
+ Box::new(self.clone())
+ }
+}
+
+impl crate::widget::ContainerLayout for AdaptiveGrid {
+ fn box_clone_container(&self) -> Box<dyn crate::widget::ContainerLayout> {
+ Box::new(self.clone())
+ }
+}
+
+impl crate::widget::ContainerLayout for RadialLayout {
+ fn box_clone_container(&self) -> Box<dyn crate::widget::ContainerLayout> {
+ Box::new(self.clone())
+ }
+}
+
#[cfg(test)]
mod tests {
use super::*;
diff --git a/src/main.rs b/src/main.rs
index 1783f56..032a285 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -322,19 +322,55 @@ fn extra_quad_vertices(
rounded_rect_vertices_corners(qx, qy, qw, qh, r, sw, sh, qc, extra_corners)
}
+#[derive(Hash, PartialEq, Eq, Clone)]
+struct BufferCacheKey {
+ text: String,
+ size_milli: u32,
+ font: Option<String>,
+}
+
+std::thread_local! {
+ static BUFFER_CACHE: std::cell::RefCell<std::collections::HashMap<BufferCacheKey, Buffer>> = std::cell::RefCell::new(std::collections::HashMap::new());
+}
+
fn make_text_buffer(font_system: &mut FontSystem, text: &str, size: f32) -> Buffer {
- let metrics = Metrics::new(size, size * 1.4);
- let mut buffer = Buffer::new(font_system, metrics);
- buffer.set_text(font_system, text, Attrs::new(), glyphon::Shaping::Advanced);
- buffer.shape_until_scroll(font_system, true);
- buffer
+ make_text_buffer_with_font(font_system, text, size, None)
}
fn make_text_buffer_with_font(font_system: &mut FontSystem, text: &str, size: f32, font: Option<&str>) -> Buffer {
- let metrics = Metrics::new(size, size * 1.4);
+ let scale = cce_ui::scale::scale_factor();
+ let mut font_size = size;
+ let mut family_name = None;
+
+ if let Some(font_str) = font {
+ let (parsed_family, parsed_size) = cce_ui::layout::parse_font_string(font_str);
+ if let Some(ps) = parsed_size {
+ font_size = ps;
+ }
+ family_name = Some(parsed_family);
+ }
+
+ let physical_size = font_size * scale;
+ let size_key = (physical_size * 1000.0).round() as u32;
+
+ let key = BufferCacheKey {
+ text: text.to_string(),
+ size_milli: size_key,
+ font: family_name.clone(),
+ };
+
+ let cached = BUFFER_CACHE.with(|cache| {
+ cache.borrow().get(&key).cloned()
+ });
+
+ if let Some(buf) = cached {
+ return buf;
+ }
+
+ let metrics = Metrics::new(physical_size, physical_size * 1.4);
let mut buffer = Buffer::new(font_system, metrics);
let mut attrs = Attrs::new();
- if let Some(font_name) = font {
+ if let Some(font_name) = family_name.as_deref() {
let family = match font_name {
"monospace" => glyphon::Family::Name(cce_ui::layout::get_system_monospace_font()),
"sans-serif" => glyphon::Family::SansSerif,
@@ -345,9 +381,15 @@ fn make_text_buffer_with_font(font_system: &mut FontSystem, text: &str, size: f3
}
buffer.set_text(font_system, text, attrs, glyphon::Shaping::Advanced);
buffer.shape_until_scroll(font_system, true);
+
+ BUFFER_CACHE.with(|cache| {
+ cache.borrow_mut().insert(key, buffer.clone());
+ });
+
buffer
}
+
struct State {
surface: wgpu::Surface<'static>,
device: wgpu::Device,
@@ -1747,20 +1789,7 @@ fn main() {
last_tick = now;
if let Some(ref mut st) = app.state {
- let mut tick_changed = false;
- if st.layout_mode {
- if let Some(ref mut jl) = &mut st.json_layout {
- if jl.tick(dt, &mut st.ui_context) {
- tick_changed = true;
- }
- }
- } else {
- for w in &mut st.widgets {
- if w.tick(dt, &mut st.ui_context) {
- tick_changed = true;
- }
- }
- }
+ let tick_changed = st.ui_context.tick(dt);
if tick_changed {
st.upload_vertices();
app.redraw = true;
diff --git a/src/widget/container/container.rs b/src/widget/container/container.rs
index 0d6b07e..05fc52f 100644
--- a/src/widget/container/container.rs
+++ b/src/widget/container/container.rs
@@ -15,7 +15,7 @@ impl Container {
parent: None,
children: Vec::new(),
base: Widget::new(),
- layout: Box::new(OverlayLayout),
+ layout: Box::new(OverlayLayout::default()),
}
}
diff --git a/src/widget/container/container_layout.rs b/src/widget/container/container_layout.rs
index d74b7e6..88c0df0 100644
--- a/src/widget/container/container_layout.rs
+++ b/src/widget/container/container_layout.rs
@@ -1,22 +1,36 @@
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>;
+pub trait ContainerLayout: crate::layout::LayoutStrategy {
+ fn box_clone_container(&self) -> Box<dyn ContainerLayout>;
}
impl Clone for Box<dyn ContainerLayout> {
fn clone(&self) -> Self {
- self.box_clone()
+ self.box_clone_container()
}
}
-#[derive(Debug, Clone, Copy)]
-pub struct OverlayLayout;
+#[derive(Debug, Clone, Copy, Default)]
+pub struct OverlayLayout {
+ left: f32,
+ top: f32,
+ width: f32,
+ height: f32,
+}
+
+impl crate::layout::LayoutStrategy for OverlayLayout {
+ fn init(&mut self, left: f32, top: f32, width: f32, height: f32) {
+ self.left = left;
+ self.top = top;
+ self.width = width;
+ self.height = height;
+ }
+
+ fn allocate(&mut self, _ww: f32, _wh: f32) -> (f32, f32, f32, f32) {
+ (self.left, self.top, self.width, self.height)
+ }
-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 {
@@ -42,7 +56,13 @@ impl ContainerLayout for OverlayLayout {
}
}
- fn box_clone(&self) -> Box<dyn ContainerLayout> {
+ fn box_clone(&self) -> Box<dyn crate::layout::LayoutStrategy> {
+ Box::new(*self)
+ }
+}
+
+impl ContainerLayout for OverlayLayout {
+ fn box_clone_container(&self) -> Box<dyn ContainerLayout> {
Box::new(*self)
}
}
@@ -64,7 +84,26 @@ impl Default for VerticalLayout {
}
}
-impl ContainerLayout for VerticalLayout {
+impl crate::layout::LayoutStrategy for VerticalLayout {
+ fn init(&mut self, left: f32, top: f32, _width: f32, _height: f32) {
+ let ptr = self as *const Self as usize;
+ crate::layout::VERTICAL_STATES.with(|m| m.borrow_mut().insert(ptr, (left, top + self.padding_y)));
+ }
+
+ fn allocate(&mut self, ww: f32, wh: f32) -> (f32, f32, f32, f32) {
+ let ptr = self as *const Self as usize;
+ let (left, mut current_y) = crate::layout::VERTICAL_STATES.with(|m| m.borrow().get(&ptr).copied().unwrap_or((0.0, 0.0)));
+ let x = left + self.padding_x;
+ let y = current_y;
+ current_y += wh + self.spacing;
+ crate::layout::VERTICAL_STATES.with(|m| m.borrow_mut().insert(ptr, (left, current_y)));
+ (x, y, ww, wh)
+ }
+
+ fn get_gap(&self) -> f32 {
+ self.spacing
+ }
+
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);
@@ -104,7 +143,13 @@ impl ContainerLayout for VerticalLayout {
}
}
- fn box_clone(&self) -> Box<dyn ContainerLayout> {
+ fn box_clone(&self) -> Box<dyn crate::layout::LayoutStrategy> {
+ Box::new(*self)
+ }
+}
+
+impl ContainerLayout for VerticalLayout {
+ fn box_clone_container(&self) -> Box<dyn ContainerLayout> {
Box::new(*self)
}
}
@@ -117,7 +162,44 @@ pub struct GridLayout {
pub padding_y: f32,
}
-impl ContainerLayout for GridLayout {
+impl crate::layout::LayoutStrategy for GridLayout {
+ fn init(&mut self, left: f32, top: f32, width: f32, _height: f32) {
+ let count = self.columns.max(1);
+ let usable_w = (width - 2.0 * self.padding_x).max(1.0);
+ let grid = crate::layout::Grid::new(
+ left + self.padding_x,
+ top + self.padding_y,
+ usable_w,
+ usable_w / count as f32,
+ self.gap,
+ count,
+ );
+ let ptr = self as *const Self as usize;
+ crate::layout::save_grid_state(ptr, grid);
+ }
+
+ fn allocate(&mut self, ww: f32, wh: f32) -> (f32, f32, f32, f32) {
+ let ptr = self as *const Self as usize;
+ crate::layout::mutate_grid_state(ptr, |grid| {
+ let col = grid.next_column();
+ let x = grid.col_lefts[col];
+ let y = grid.col_heights[col];
+ grid.col_heights[col] += wh + grid.gap;
+ (x, y, grid.col_width, wh)
+ }).unwrap_or((0.0, 0.0, ww, wh))
+ }
+
+ fn get_column_width(&self) -> Option<f32> {
+ let ptr = self as *const Self as usize;
+ crate::layout::GRID_STATES.with(|m| {
+ m.borrow().get(&ptr).map(|g| g.col_width)
+ })
+ }
+
+ fn get_gap(&self) -> f32 {
+ self.gap
+ }
+
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 {
@@ -187,7 +269,13 @@ impl ContainerLayout for GridLayout {
}
}
- fn box_clone(&self) -> Box<dyn ContainerLayout> {
+ fn box_clone(&self) -> Box<dyn crate::layout::LayoutStrategy> {
+ Box::new(*self)
+ }
+}
+
+impl ContainerLayout for GridLayout {
+ fn box_clone_container(&self) -> Box<dyn ContainerLayout> {
Box::new(*self)
}
}
@@ -200,7 +288,44 @@ pub struct AdaptiveGridLayout {
pub padding_y: f32,
}
-impl ContainerLayout for AdaptiveGridLayout {
+impl crate::layout::LayoutStrategy for AdaptiveGridLayout {
+ fn init(&mut self, left: f32, top: f32, width: f32, _height: f32) {
+ let usable_w = (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 = crate::layout::Grid::new(
+ left + self.padding_x,
+ top + self.padding_y,
+ usable_w,
+ self.min_col_width,
+ self.gap,
+ cols,
+ );
+ let ptr = self as *const Self as usize;
+ crate::layout::save_grid_state(ptr, grid);
+ }
+
+ fn allocate(&mut self, ww: f32, wh: f32) -> (f32, f32, f32, f32) {
+ let ptr = self as *const Self as usize;
+ crate::layout::mutate_grid_state(ptr, |grid| {
+ let col = grid.next_column();
+ let x = grid.col_lefts[col];
+ let y = grid.col_heights[col];
+ grid.col_heights[col] += wh + grid.gap;
+ (x, y, grid.col_width, wh)
+ }).unwrap_or((0.0, 0.0, ww, wh))
+ }
+
+ fn get_column_width(&self) -> Option<f32> {
+ let ptr = self as *const Self as usize;
+ crate::layout::GRID_STATES.with(|m| {
+ m.borrow().get(&ptr).map(|g| g.col_width)
+ })
+ }
+
+ fn get_gap(&self) -> f32 {
+ self.gap
+ }
+
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;
@@ -225,7 +350,13 @@ impl ContainerLayout for AdaptiveGridLayout {
grid.measure(constraints, children, ctx)
}
- fn box_clone(&self) -> Box<dyn ContainerLayout> {
+ fn box_clone(&self) -> Box<dyn crate::layout::LayoutStrategy> {
+ Box::new(*self)
+ }
+}
+
+impl ContainerLayout for AdaptiveGridLayout {
+ fn box_clone_container(&self) -> Box<dyn ContainerLayout> {
Box::new(*self)
}
}
diff --git a/src/widget/container/paginator.rs b/src/widget/container/paginator.rs
index 9b9a943..dc487c2 100644
--- a/src/widget/container/paginator.rs
+++ b/src/widget/container/paginator.rs
@@ -253,6 +253,18 @@ impl Element for Paginator {
childs
}
+ fn is_child_visible(&self, child_id: WidgetId) -> bool {
+ if self.sidebar_menu.base.id() == child_id {
+ return true;
+ }
+ if let Some(plate) = self.pages.get(self.selected_page) {
+ if !self.page_hidden && plate.base.base.id() == child_id {
+ return true;
+ }
+ }
+ false
+ }
+
fn cursor_moved(&mut self, px: f32, py: f32, ctx: &mut UiContext) -> bool {
let mut changed = false;
if self.sidebar_menu.cursor_moved(px, py, ctx) {
diff --git a/src/widget/container/spreadsheet.rs b/src/widget/container/spreadsheet.rs
index 79e0e4c..8f1b3bb 100644
--- a/src/widget/container/spreadsheet.rs
+++ b/src/widget/container/spreadsheet.rs
@@ -265,6 +265,10 @@ impl Element for Spreadsheet {
}
}
+ fn wants_tick(&self) -> bool {
+ true
+ }
+
fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
if !self.visible {
return Vec::new();
diff --git a/src/widget/container/switcher.rs b/src/widget/container/switcher.rs
index dd0f1a2..f541615 100644
--- a/src/widget/container/switcher.rs
+++ b/src/widget/container/switcher.rs
@@ -68,6 +68,17 @@ impl Element for Switcher {
self.children.clone()
}
+ fn is_child_visible(&self, child_id: WidgetId) -> bool {
+ if let Some(idx) = self.active_index {
+ if idx < self.children.len() {
+ if let Some(b) = unsafe { (*self.children[idx]).base() } {
+ return b.id() == child_id;
+ }
+ }
+ }
+ false
+ }
+
fn add_child(&mut self, child: *mut (dyn Element + 'static), ctx: &mut UiContext) {
self.children.push(child);
let id = self.base.id();
diff --git a/src/widget/input/button_strip.rs b/src/widget/input/button_strip.rs
index aa00738..2c87f1a 100644
--- a/src/widget/input/button_strip.rs
+++ b/src/widget/input/button_strip.rs
@@ -264,6 +264,10 @@ impl Element for ButtonStrip {
changed
}
+ fn wants_tick(&self) -> bool {
+ true
+ }
+
fn highlight_quad(&self, _ctx: &UiContext) -> Option<(f32, f32, f32, f32, [f32; 4])> {
None
}
diff --git a/src/widget/input/color_selector.rs b/src/widget/input/color_selector.rs
index 026aa57..07850f5 100644
--- a/src/widget/input/color_selector.rs
+++ b/src/widget/input/color_selector.rs
@@ -259,6 +259,10 @@ impl Element for ColorSelector {
false
}
+ fn wants_tick(&self) -> bool {
+ true
+ }
+
fn focus(&mut self) {
self.editing = true;
self.edit_buffer = self.get_value_string().unwrap();
diff --git a/src/widget/json_layout.rs b/src/widget/json_layout.rs
index b74a31c..38fb55e 100644
--- a/src/widget/json_layout.rs
+++ b/src/widget/json_layout.rs
@@ -309,6 +309,10 @@ impl Element for JsonLayoutWidget {
changed
}
+ fn wants_tick(&self) -> bool {
+ true
+ }
+
fn all_quads(&self, ctx: &UiContext) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
let mut quads = Vec::new();
diff --git a/src/widget/mod.rs b/src/widget/mod.rs
index b7d5726..d26bf73 100644
--- a/src/widget/mod.rs
+++ b/src/widget/mod.rs
@@ -69,7 +69,7 @@ use std::collections::HashMap;
pub const DROPDOWN_ITEM_H: f32 = 22.0;
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct WidgetId(pub usize);
pub static NEXT_WIDGET_ID: AtomicUsize = AtomicUsize::new(1);
@@ -80,6 +80,33 @@ pub struct LayoutTree {
pub children: HashMap<WidgetId, Vec<WidgetId>>,
}
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
+pub struct WidgetPtr(pub *mut (dyn Element + 'static));
+
+impl WidgetPtr {
+ pub fn is_null(&self) -> bool {
+ self.0.is_null()
+ }
+ pub fn as_ptr(&self) -> *mut (dyn Element + 'static) {
+ self.0
+ }
+}
+
+impl std::ops::Deref for WidgetPtr {
+ type Target = dyn Element + 'static;
+ fn deref(&self) -> &Self::Target {
+ assert!(!self.0.is_null(), "Attempted to dereference a null WidgetPtr!");
+ unsafe { &*self.0 }
+ }
+}
+
+impl std::ops::DerefMut for WidgetPtr {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ assert!(!self.0.is_null(), "Attempted to dereference a null WidgetPtr!");
+ unsafe { &mut *self.0 }
+ }
+}
+
pub use crate::context::UiContext;
#[derive(Debug, Clone, PartialEq)]
@@ -470,6 +497,8 @@ pub trait Element {
fn set_visible(&mut self, _visible: bool) {}
fn visible(&self) -> bool { true }
fn tick(&mut self, _dt: f32, _ctx: &mut UiContext) -> bool { false }
+ fn wants_tick(&self) -> bool { false }
+ fn is_child_visible(&self, _child_id: WidgetId) -> bool { true }
fn set_modifiers(&mut self, _ctrl: bool, _shift: bool, _alt: bool) {}
fn as_page_selector(&self) -> Option<&dyn PageSelector> { None }