GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
Implement reactive updates, event propagation, layout and clipping improvements
src/backend/window_runner.rs | 26 +++++++++-
src/context.rs | 71 ++++++++++++++++++++++++-
src/layout.rs | 104 +++++++++++++++++++++++++++++++++++++
src/widget/container/container.rs | 33 ++++++++++--
src/widget/container/menu.rs | 30 +++++++----
src/widget/container/paginator.rs | 6 ++-
src/widget/core.rs | 3 ++
src/widget/display/mod.rs | 2 +
src/widget/display/text_label.rs | 7 +--
src/widget/display/text_sizer.rs | 102 ++++++++++++++++++++++++++++++++++++
src/widget/input/button.rs | 2 +-
src/widget/input/button_strip.rs | 50 +++++-------------
src/widget/input/checkbox.rs | 2 +-
src/widget/input/color_selector.rs | 2 +-
src/widget/mod.rs | 20 +++++++
15 files changed, 398 insertions(+), 62 deletions(-)
diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index 765c7ff..3e8e9a6 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -1473,8 +1473,14 @@ impl<A: Application> OutputHandler for EngineState<A> {
&mut self.output_state
}
- fn new_output(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _output: wl_output::WlOutput) {}
- fn update_output(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _output: wl_output::WlOutput) {}
+ fn new_output(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _output: wl_output::WlOutput) {
+ let scale = crate::wayland::detect_scale_factor(&self.output_state);
+ crate::scale::set_scale_factor(scale as f32);
+ }
+ fn update_output(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _output: wl_output::WlOutput) {
+ let scale = crate::wayland::detect_scale_factor(&self.output_state);
+ crate::scale::set_scale_factor(scale as f32);
+ }
fn output_destroyed(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _output: wl_output::WlOutput) {}
}
@@ -1789,6 +1795,22 @@ impl<A: Application> KeyboardHandler for EngineState<A> {
self.ctrl_pressed = modifiers.ctrl;
self.shift_pressed = modifiers.shift;
}
+
+ fn update_repeat_info(
+ &mut self,
+ _conn: &Connection,
+ _qh: &QueueHandle<Self>,
+ _keyboard: &wl_keyboard::WlKeyboard,
+ info: smithay_client_toolkit::seat::keyboard::RepeatInfo,
+ ) {
+ match info {
+ smithay_client_toolkit::seat::keyboard::RepeatInfo::Repeat { rate, delay } => {
+ // Store/expose delay/rate if required by the application
+ let _ = (rate, delay);
+ }
+ smithay_client_toolkit::seat::keyboard::RepeatInfo::Disable => {}
+ }
+ }
}
impl<A: Application> EngineState<A> {
diff --git a/src/context.rs b/src/context.rs
index da0b9c3..3236f80 100644
--- a/src/context.rs
+++ b/src/context.rs
@@ -1,5 +1,5 @@
use std::collections::HashMap;
-use crate::widget::{Element, WidgetId, LayoutTree, Key, MouseButton, ElementState};
+use crate::widget::{Element, WidgetId, LayoutTree, Key, MouseButton, ElementState, Event};
use crate::widget::core::hover_animation::HoverState;
use crate::widget::core::context_menu::ContextMenuState;
@@ -29,6 +29,75 @@ impl UiContext {
}
}
+ pub fn propagate_event(&mut self, event: &Event, root: *mut (dyn Element + 'static)) -> bool {
+ if root.is_null() {
+ return false;
+ }
+ unsafe {
+ // For KeyInput, send directly to focused widget if it exists
+ if let Event::KeyInput(_) = event {
+ if let Some(focused) = self.focused_widget {
+ if (*focused).handle_event(event, self) {
+ (*focused).mark_dirty(self);
+ return true;
+ }
+ }
+ }
+
+ let mut handled = false;
+ let children = (*root).children(self);
+
+ match event {
+ Event::PointerMove { .. } => {
+ for child in children.into_iter().rev() {
+ if self.propagate_event(event, child) {
+ handled = true;
+ }
+ }
+ if (*root).handle_event(event, self) {
+ (*root).mark_dirty(self);
+ handled = true;
+ }
+ }
+ _ => {
+ for child in children.into_iter().rev() {
+ if self.propagate_event(event, child) {
+ return true;
+ }
+ }
+ if (*root).handle_event(event, self) {
+ (*root).mark_dirty(self);
+ return true;
+ }
+ }
+ }
+ handled
+ }
+ }
+
+ pub fn is_dirty(&self) -> bool {
+ for &ptr in self.widget_registry.values() {
+ unsafe {
+ if let Some(b) = (*ptr).base() {
+ if b.dirty {
+ return true;
+ }
+ }
+ }
+ }
+ false
+ }
+
+ pub fn clear_dirty(&mut self) {
+ for &ptr in self.widget_registry.values() {
+ unsafe {
+ if let Some(b) = (*ptr).base_mut() {
+ b.dirty = false;
+ }
+ }
+ }
+ }
+
// --- Focus management ---
pub fn set_focused(&mut self, w: &mut dyn Element) {
let new_ptr = unsafe {
diff --git a/src/layout.rs b/src/layout.rs
index 0b654b5..81fcce2 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -89,6 +89,7 @@ static BREADCRUMB_FONT: RwLock<String> = RwLock::new(String::new());
static PAGINATOR_TAB_PADDING_X: RwLock<f32> = RwLock::new(10.0);
static BUTTON_PADDING: RwLock<f32> = RwLock::new(14.0);
+static BUTTON_STRIP_SPACING: RwLock<f32> = RwLock::new(8.0);
static PLATE_PADDING: RwLock<f32> = RwLock::new(20.0);
static DROPDOWN_HEIGHT: RwLock<f32> = RwLock::new(44.0);
@@ -426,6 +427,15 @@ pub fn reload_config() {
}
}
}
+ if let Some(rest) = trimmed.strip_prefix("button_strip_spacing") {
+ let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
+ let val_str = rest.trim_end_matches('"').trim();
+ if let Ok(val) = val_str.parse::<f32>() {
+ if let Ok(mut lock) = BUTTON_STRIP_SPACING.write() {
+ *lock = val;
+ }
+ }
+ }
if let Some(rest) = trimmed.strip_prefix("textbox_height") {
let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
let val_str = rest.trim_end_matches('"').trim();
@@ -1446,6 +1456,34 @@ pub fn set_button_padding(padding: f32) {
}
}
+pub fn button_strip_spacing() -> f32 {
+ use std::sync::Once;
+ static INIT: Once = Once::new();
+ INIT.call_once(|| {
+ if let Some(content) = read_config() {
+ for line in content.lines() {
+ let trimmed = line.trim();
+ if let Some(rest) = trimmed.strip_prefix("button_strip_spacing") {
+ let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
+ let val_str = rest.trim_end_matches('"').trim();
+ if let Ok(val) = val_str.parse::<f32>() {
+ if let Ok(mut lock) = BUTTON_STRIP_SPACING.write() {
+ *lock = val;
+ }
+ }
+ }
+ }
+ }
+ });
+ *BUTTON_STRIP_SPACING.read().unwrap()
+}
+
+pub fn set_button_strip_spacing(spacing: f32) {
+ if let Ok(mut lock) = BUTTON_STRIP_SPACING.write() {
+ *lock = spacing;
+ }
+}
+
pub fn paginator_tab_padding_y() -> f32 {
button_padding()
}
@@ -1557,6 +1595,8 @@ pub trait RenderTarget {
fn text_with_font_and_bounds(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], font: &str, _bounds: Option<[f32; 4]>) {
self.text_with_font(content, x, y, size, color, font);
}
+ fn push_clip_rect(&mut self, _x: f32, _y: f32, _w: f32, _h: f32) {}
+ fn pop_clip_rect(&mut self) {}
}
pub struct PopoverCollector {
@@ -2482,6 +2522,70 @@ pub trait LayoutStrategy {
fn get_gap(&self) -> f32 { 20.0 }
}
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum FlexDirection {
+ Row,
+ Column,
+}
+
+pub struct FlexLayout {
+ left: f32,
+ top: f32,
+ width: f32,
+ height: f32,
+ direction: FlexDirection,
+ spacing: f32,
+ current_x: f32,
+ current_y: f32,
+}
+
+impl FlexLayout {
+ pub fn new(direction: FlexDirection, spacing: f32) -> Self {
+ Self {
+ left: 0.0,
+ top: 0.0,
+ width: 0.0,
+ height: 0.0,
+ direction,
+ spacing,
+ current_x: 0.0,
+ current_y: 0.0,
+ }
+ }
+}
+
+impl LayoutStrategy for FlexLayout {
+ fn init(&mut self, left: f32, top: f32, width: f32, height: f32) {
+ self.left = left;
+ self.top = top;
+ self.width = width;
+ self.height = height;
+ self.current_x = left;
+ self.current_y = top;
+ }
+
+ fn allocate(&mut self, ww: f32, wh: f32) -> (f32, f32, f32, f32) {
+ match self.direction {
+ FlexDirection::Row => {
+ let rx = self.current_x;
+ let ry = self.current_y;
+ self.current_x += ww + self.spacing;
+ (rx, ry, ww, wh)
+ }
+ FlexDirection::Column => {
+ let rx = self.current_x;
+ let ry = self.current_y;
+ self.current_y += wh + self.spacing;
+ (rx, ry, ww, wh)
+ }
+ }
+ }
+
+ fn get_gap(&self) -> f32 {
+ self.spacing
+ }
+}
+
pub struct ColumnLayout {
left: f32,
top: f32,
diff --git a/src/widget/container/container.rs b/src/widget/container/container.rs
index 87b3e22..9533484 100644
--- a/src/widget/container/container.rs
+++ b/src/widget/container/container.rs
@@ -4,17 +4,18 @@ use crate::widget::*;
pub struct Container {
pub parent: Option<*mut (dyn Element + 'static)>,
pub children: Vec<*mut (dyn Element + 'static)>,
+ pub base: Widget,
}
impl Container {
pub fn new() -> Self {
- Self { parent: None, children: Vec::new() }
+ Self { parent: None, children: Vec::new(), base: Widget::new() }
}
}
impl Element for Container {
- fn rect(&self) -> (f32, f32, f32, f32) { (0.0, 0.0, 0.0, 0.0) }
- fn set_rect(&mut self, _x: f32, _y: f32, _w: f32, _h: f32) {}
+ fn base(&self) -> Option<&Widget> { Some(&self.base) }
+ 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)
@@ -23,6 +24,32 @@ impl Element for Container {
self as *mut Self as *mut (dyn Element + 'static)
}
+ 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),
+ }
+ }
+
+ 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);
+ }
+ }
+ }
+
fn focus(&mut self) {
focus::set_focused(self);
}
diff --git a/src/widget/container/menu.rs b/src/widget/container/menu.rs
index 4dffcbf..325cfd9 100644
--- a/src/widget/container/menu.rs
+++ b/src/widget/container/menu.rs
@@ -352,8 +352,9 @@ impl Element for MenuBar {
let parent_ptr = self as *mut MenuBar as *mut (dyn Element + 'static);
let font_setting = crate::layout::menubar_font();
- let (_, font_size_opt) = crate::layout::parse_font_string(&font_setting);
- let font_size = font_size_opt.unwrap_or(12.0);
+ let font_info = crate::layout::parse_font_string(&font_setting);
+ let font_fam = font_info.0;
+ let font_size = font_info.1.unwrap_or(12.0);
if self.vertical {
let mut cy = 16.0;
@@ -381,6 +382,7 @@ impl Element for MenuBar {
self.menus.set_parent(Some(parent_ptr), &mut dummy);
} else {
let padding = crate::layout::button_padding();
+ let spacing = crate::layout::button_strip_spacing();
let mut cx = 8.0;
if self.center_items {
let mut total_width = 8.0;
@@ -389,12 +391,15 @@ impl Element for MenuBar {
if !self.context_options.is_empty() {
display_title.push_str(" ▼");
}
- total_width += TextLabel::estimate_width(&display_title, font_size) + 24.0;
+ total_width += crate::widget::display::measure_text_width(&display_title, &font_fam, font_size) + 24.0;
}
let mut btn_strip_w = 0.0;
- for btn_label in &self.menus.buttons {
- let text_w = TextLabel::estimate_width(btn_label, font_size);
+ for (i, btn_label) in self.menus.buttons.iter().enumerate() {
+ let text_w = crate::widget::display::measure_text_width(btn_label, &font_fam, font_size);
btn_strip_w += text_w + 2.0 * padding;
+ if i > 0 {
+ btn_strip_w += spacing;
+ }
}
total_width += btn_strip_w;
if self.base.w > total_width {
@@ -406,12 +411,15 @@ impl Element for MenuBar {
if !self.context_options.is_empty() {
display_title.push_str(" ▼");
}
- cx += TextLabel::estimate_width(&display_title, font_size) + 24.0;
+ cx += crate::widget::display::measure_text_width(&display_title, &font_fam, font_size) + 24.0;
}
let mut btn_strip_w = 0.0;
- for btn_label in &self.menus.buttons {
- let text_w = TextLabel::estimate_width(btn_label, font_size);
+ for (i, btn_label) in self.menus.buttons.iter().enumerate() {
+ let text_w = crate::widget::display::measure_text_width(btn_label, &font_fam, font_size);
btn_strip_w += text_w + 2.0 * padding;
+ if i > 0 {
+ btn_strip_w += spacing;
+ }
}
let menus_x = (clamped_x + cx).clamp(clamped_x, clamped_x + clamped_w);
let menus_w = btn_strip_w.min(clamped_x + clamped_w - menus_x);
@@ -739,7 +747,7 @@ impl Element for MenuBar {
let start_y = self.base.y + 16.0;
for (i, c) in label.chars().enumerate() {
let char_str = c.to_string();
- let char_w = TextLabel::estimate_width(&char_str, font_size);
+ let char_w = crate::widget::display::measure_text(&char_str, font_size);
let x_pos = self.base.x + (self.base.w - char_w) / 2.0;
let y_pos = start_y + i as f32 * line_height;
labels.push(TextLabel {
@@ -797,7 +805,7 @@ impl Element for MenuBar {
start_y += label_h + 20.0;
}
let line_height = font_size * 1.2;
- let char_w = TextLabel::estimate_width("o", font_size);
+ let char_w = crate::widget::display::measure_text("o", font_size);
let x_pos = self.base.x + (self.base.w - char_w) / 2.0;
let mut display_title_vertical = self.title.clone();
if !self.context_options.is_empty() {
@@ -1203,7 +1211,7 @@ impl Element for Menu {
let label_len = title.chars().count() as f32;
let total_h = label_len * line_height;
let start_y = self.base.y + (self.base.h - total_h) / 2.0;
- let char_w = TextLabel::estimate_width("o", font_size);
+ let char_w = crate::widget::display::measure_text("o", font_size);
let x_pos = self.base.x + (self.base.w - char_w) / 2.0;
for (i, c) in title.chars().enumerate() {
let char_str = c.to_string();
diff --git a/src/widget/container/paginator.rs b/src/widget/container/paginator.rs
index c6eeaf5..23c1435 100644
--- a/src/widget/container/paginator.rs
+++ b/src/widget/container/paginator.rs
@@ -139,7 +139,9 @@ impl PageSelector for Paginator {
if self.pages.is_empty() {
return self.sidebar_w;
}
- let font_size = crate::layout::menubar_font_parsed().1;
+ let font_info = crate::layout::menubar_font_parsed();
+ let font_fam = font_info.0;
+ let font_size = font_info.1;
let padding = crate::layout::button_padding();
let mut max_w = 0.0;
@@ -152,7 +154,7 @@ impl PageSelector for Paginator {
let (icon, _) = trimmed.split_at(space_idx);
let icon = icon.trim();
let icon_font_size = 14.0;
- let est_icon_w = TextLabel::estimate_width(icon, icon_font_size);
+ let est_icon_w = crate::widget::display::measure_text_width(icon, &font_fam, icon_font_size);
est_icon_w.max(font_size)
} else {
font_size
diff --git a/src/widget/core.rs b/src/widget/core.rs
index 6eba037..7b1cd27 100644
--- a/src/widget/core.rs
+++ b/src/widget/core.rs
@@ -671,6 +671,7 @@ pub struct Widget {
pub row_w: f32,
pub focused: bool,
pub id: std::cell::Cell<Option<crate::widget::WidgetId>>,
+ pub dirty: bool,
}
impl Widget {
@@ -686,6 +687,7 @@ impl Widget {
row_w: 0.0,
focused: false,
id: std::cell::Cell::new(None),
+ dirty: true,
}
}
@@ -701,6 +703,7 @@ impl Widget {
row_w: 0.0,
focused: false,
id: std::cell::Cell::new(None),
+ dirty: true,
}
}
diff --git a/src/widget/display/mod.rs b/src/widget/display/mod.rs
index d1bf2be..8f344d8 100644
--- a/src/widget/display/mod.rs
+++ b/src/widget/display/mod.rs
@@ -18,6 +18,7 @@ pub mod font_preview;
pub mod info_box;
pub mod status_dot;
pub mod preview;
+pub mod text_sizer;
pub use self::text_label::TextLabel;
pub(crate) use self::text_label::make_widget_text_buffer;
@@ -40,3 +41,4 @@ pub use self::font_preview::FontPreview;
pub use self::info_box::InfoBox;
pub use self::status_dot::{DotStatus, StatusDot};
pub use self::preview::PreviewState;
+pub use self::text_sizer::{measure_text_width, measure_text};
diff --git a/src/widget/display/text_label.rs b/src/widget/display/text_label.rs
index 5281181..5d7e625 100644
--- a/src/widget/display/text_label.rs
+++ b/src/widget/display/text_label.rs
@@ -34,8 +34,9 @@ impl TextLabel {
color: [u8; 3],
) -> Vec<TextLabel> {
let mut labels = Vec::new();
+ let font_fam = crate::layout::menubar_font_parsed().0;
let char_widths: Vec<f32> = text.chars().map(|c| {
- Self::estimate_width(&c.to_string(), font_size)
+ crate::widget::display::measure_text_width(&c.to_string(), &font_fam, font_size)
}).collect();
let total_width: f32 = char_widths.iter().sum();
@@ -64,9 +65,9 @@ impl TextLabel {
}
labels
}
-
+
pub fn is_covered_by(&self, px: f32, py: f32, pw: f32, ph: f32) -> bool {
- let text_w = Self::estimate_width(&self.text, self.font_size);
+ let text_w = crate::widget::display::measure_text(&self.text, self.font_size);
let x_overlap = self.x <= px + pw && (self.x + text_w) >= px;
let y_overlap = self.y <= py + ph && (self.y + self.font_size) >= py;
x_overlap && y_overlap
diff --git a/src/widget/display/text_sizer.rs b/src/widget/display/text_sizer.rs
new file mode 100644
index 0000000..753a624
--- /dev/null
+++ b/src/widget/display/text_sizer.rs
@@ -0,0 +1,102 @@
+use std::collections::HashMap;
+use std::sync::RwLock;
+use std::sync::OnceLock;
+use crate::widget::display::TextLabel;
+
+#[derive(Hash, Eq, PartialEq, Clone, Debug)]
+struct TextMeasureKey {
+ text: String,
+ font_family: String,
+ font_size_bits: u32,
+ scale_bits: u32,
+}
+
+static TEXT_SIZE_CACHE: OnceLock<RwLock<HashMap<TextMeasureKey, f32>>> = OnceLock::new();
+
+pub fn measure_text_width(text: &str, font_family: &str, font_size: f32) -> f32 {
+ let scale = crate::scale::scale_factor().max(1.0);
+
+ let key = TextMeasureKey {
+ text: text.trim().to_string(),
+ font_family: font_family.to_string(),
+ font_size_bits: font_size.to_bits(),
+ scale_bits: scale.to_bits(),
+ };
+
+ let cache = TEXT_SIZE_CACHE.get_or_init(|| RwLock::new(HashMap::new()));
+ if let Ok(lock) = cache.read() {
+ if let Some(&exact_width) = lock.get(&key) {
+ return exact_width;
+ }
+ }
+
+ let exact_width = perform_svg_measurement(&key.text, &key.font_family, font_size, scale);
+
+ if let Ok(mut lock) = cache.write() {
+ lock.insert(key, exact_width);
+ }
+
+ exact_width
+}
+
+pub fn measure_text(text: &str, font_size: f32) -> f32 {
+ let font_family = crate::layout::menubar_font_parsed().0;
+ measure_text_width(text, &font_family, font_size)
+}
+
+fn perform_svg_measurement(text: &str, font_family: &str, font_size: f32, scale: f32) -> f32 {
+ if text.is_empty() {
+ return 0.0;
+ }
+
+ let canvas_w = 1000.0;
+ let canvas_h = font_size * 2.5;
+
+ let w_px = (canvas_w * scale) as u32;
+ let h_px = (canvas_h * scale) as u32;
+
+ let svg_data = format!(
+ r##"<svg width="{}" height="{}" viewBox="0 0 {} {}" xmlns="http://www.w3.org/2000/svg">
+ <text x="{}" y="{}" font-family="{}" font-size="{}" fill="#000000" text-anchor="middle" dominant-baseline="middle">{}</text>
+</svg>"##,
+ w_px, h_px,
+ canvas_w, canvas_h,
+ canvas_w / 2.0, canvas_h / 2.0,
+ font_family,
+ font_size,
+ text
+ );
+
+ let opt = resvg::usvg::Options::default();
+ let fontdb = crate::widget::input::get_font_db();
+
+ if let Ok(tree) = resvg::usvg::Tree::from_data(svg_data.as_bytes(), &opt, fontdb) {
+ if let Some(mut pixmap) = resvg::tiny_skia::Pixmap::new(w_px, h_px) {
+ resvg::render(&tree, resvg::tiny_skia::Transform::default(), &mut pixmap.as_mut());
+ let pixels = pixmap.data();
+
+ let mut min_col = None;
+ let mut max_col = None;
+
+ for row in 0..h_px {
+ for col in 0..w_px {
+ let idx = ((row * w_px + col) * 4) as usize;
+ if idx + 3 < pixels.len() && pixels[idx + 3] > 0 {
+ if min_col.is_none() || col < min_col.unwrap() {
+ min_col = Some(col);
+ }
+ if max_col.is_none() || col > max_col.unwrap() {
+ max_col = Some(col);
+ }
+ }
+ }
+ }
+
+ if let (Some(min), Some(max)) = (min_col, max_col) {
+ return (max - min + 1) as f32 / scale;
+ }
+ }
+ }
+
+ TextLabel::estimate_width(text, font_size)
+}
diff --git a/src/widget/input/button.rs b/src/widget/input/button.rs
index c795e2d..2099632 100644
--- a/src/widget/input/button.rs
+++ b/src/widget/input/button.rs
@@ -218,7 +218,7 @@ impl Element for Button {
let est_w = if label == "📋" {
12.0
} else {
- TextLabel::estimate_width(label, font_size)
+ crate::widget::display::measure_text(label, font_size)
};
let color = if let Some(lc) = self.label_color {
[
diff --git a/src/widget/input/button_strip.rs b/src/widget/input/button_strip.rs
index e4ee090..aa00738 100644
--- a/src/widget/input/button_strip.rs
+++ b/src/widget/input/button_strip.rs
@@ -14,8 +14,6 @@ pub struct ButtonStrip {
pub tab_text_quads: Vec<Vec<(f32, f32, f32, f32, [f32; 4])>>,
pub tab_quads_cache: std::collections::HashMap<String, Vec<(f32, f32, f32, f32, [f32; 4])>>,
pub last_padding: Option<f32>,
- pub tab_exact_widths: std::collections::HashMap<String, f32>,
- pub needs_relayout: bool,
}
impl ButtonStrip {
@@ -31,8 +29,6 @@ impl ButtonStrip {
tab_text_quads: Vec::new(),
tab_quads_cache: std::collections::HashMap::new(),
last_padding: None,
- tab_exact_widths: std::collections::HashMap::new(),
- needs_relayout: false,
}
}
@@ -154,17 +150,13 @@ impl ButtonStrip {
if let Some(mut pixmap) = resvg::tiny_skia::Pixmap::new(w_px, h_px) {
resvg::render(&tree, resvg::tiny_skia::Transform::default(), &mut pixmap.as_mut());
let pixels = pixmap.data();
- let mut first_row = None;
- let mut last_row = None;
-
+
for row in 0..h_px {
- let mut row_has_pixel = false;
for col in 0..w_px {
let idx = ((row * w_px + col) * 4) as usize;
if idx + 3 < pixels.len() {
let a = pixels[idx + 3] as f32 / 255.0;
if a > 0.0 {
- row_has_pixel = true;
let r = ((pixels[idx] as f32 / 255.0) / a).min(1.0);
let g = ((pixels[idx + 1] as f32 / 255.0) / a).min(1.0);
let b = ((pixels[idx + 2] as f32 / 255.0) / a).min(1.0);
@@ -178,21 +170,6 @@ impl ButtonStrip {
}
}
}
- if row_has_pixel {
- if first_row.is_none() {
- first_row = Some(row);
- }
- last_row = Some(row);
- }
- }
-
- if let (Some(first), Some(last)) = (first_row, last_row) {
- let exact_h = (last - first + 1) as f32 / scale;
- let cached = self.tab_exact_widths.get(label_text);
- if cached != Some(&exact_h) {
- self.tab_exact_widths.insert(label_text.to_string(), exact_h);
- self.needs_relayout = true;
- }
}
}
}
@@ -210,7 +187,9 @@ impl ButtonStrip {
let get_button_weight = |i: usize| -> f32 {
let label = &self.buttons[i];
let trimmed = label.trim();
- let font_size = crate::layout::menubar_font_parsed().1;
+ let font_info = crate::layout::menubar_font_parsed();
+ let font_fam = font_info.0;
+ let font_size = font_info.1;
let padding = crate::layout::button_padding();
if self.vertical {
let space_idx = trimmed.find(' ');
@@ -220,15 +199,14 @@ impl ButtonStrip {
} else {
trimmed
};
- let text_w = self.tab_exact_widths.get(label_text).copied()
- .unwrap_or_else(|| TextLabel::estimate_width(label_text, font_size));
+ let text_w = crate::widget::display::measure_text_width(label_text, &font_fam, font_size);
if has_icon {
(text_w + 12.0 + 3.0 * padding).max(1.0)
} else {
(text_w + 2.0 * padding).max(1.0)
}
} else {
- let text_w = TextLabel::estimate_width(label, font_size);
+ let text_w = crate::widget::display::measure_text_width(label, &font_fam, font_size);
(text_w + 2.0 * padding).max(1.0)
}
};
@@ -238,8 +216,8 @@ impl ButtonStrip {
weights.push(get_button_weight(i));
}
+ let spacing = crate::layout::button_strip_spacing();
if self.vertical {
- let spacing = 8.0;
let mut current_y = y;
let mut btn_h = 0.0;
for i in 0..=idx {
@@ -255,7 +233,7 @@ impl ButtonStrip {
for i in 0..=idx {
btn_w = weights[i];
if i < idx {
- current_x += btn_w;
+ current_x += btn_w + spacing;
}
}
(current_x, y, btn_w, h)
@@ -283,10 +261,6 @@ impl Element for ButtonStrip {
self.generate_rotated_labels();
changed = true;
}
- if self.needs_relayout {
- self.needs_relayout = false;
- changed = true;
- }
changed
}
@@ -395,7 +369,9 @@ impl Element for ButtonStrip {
fn text_labels(&self) -> Vec<TextLabel> {
let mut labels = Vec::new();
- let font_size = 12.0;
+ let font_info = crate::layout::menubar_font_parsed();
+ let font_fam = font_info.0;
+ let font_size = font_info.1;
for (i, btn_label) in self.buttons.iter().enumerate() {
let r = self.item_rect(i);
let color = if Some(i) == self.selected {
@@ -413,7 +389,7 @@ impl Element for ButtonStrip {
let icon = icon.trim();
if !icon.is_empty() {
let icon_font_size = 14.0;
- let est_icon_w = TextLabel::estimate_width(icon, icon_font_size);
+ let est_icon_w = crate::widget::display::measure_text_width(icon, &font_fam, icon_font_size);
let padding_y = crate::layout::button_padding();
let icon_y = r.1 + (padding_y - 2.0).max(0.0);
labels.push(TextLabel {
@@ -426,7 +402,7 @@ impl Element for ButtonStrip {
}
}
} else {
- let est_w = TextLabel::estimate_width(btn_label, font_size);
+ let est_w = crate::widget::display::measure_text_width(btn_label, &font_fam, font_size);
labels.push(TextLabel {
text: btn_label.clone(),
x: r.0 + (r.2 - est_w) / 2.0,
diff --git a/src/widget/input/checkbox.rs b/src/widget/input/checkbox.rs
index 0081881..ffd9464 100644
--- a/src/widget/input/checkbox.rs
+++ b/src/widget/input/checkbox.rs
@@ -276,7 +276,7 @@ impl Element for Toggle {
let mut labels = Vec::new();
if let Some(ref label) = self.base.label {
let font_size = 12.0;
- let est_w = TextLabel::estimate_width(label, font_size);
+ let est_w = crate::widget::display::measure_text(label, font_size);
labels.push(TextLabel {
text: label.clone(),
x: self.base.x + (self.base.w - est_w) / 2.0,
diff --git a/src/widget/input/color_selector.rs b/src/widget/input/color_selector.rs
index 4567d11..134d00b 100644
--- a/src/widget/input/color_selector.rs
+++ b/src/widget/input/color_selector.rs
@@ -357,7 +357,7 @@ impl Element for ColorSelector {
if self.editing {
let font_size = 12.0;
let cursor_text: String = self.edit_buffer.chars().take(self.cursor_idx).collect();
- let text_w = TextLabel::estimate_width(&cursor_text, font_size);
+ let text_w = crate::widget::display::measure_text(&cursor_text, font_size);
let caret_x = self.base.x + 4.0 + text_w;
let caret_h = font_size * 1.15;
let caret_y = self.base.y + top + (visual_h - caret_h) / 2.0;
diff --git a/src/widget/mod.rs b/src/widget/mod.rs
index 5af7410..226f9cb 100644
--- a/src/widget/mod.rs
+++ b/src/widget/mod.rs
@@ -126,6 +126,26 @@ pub trait Element {
fn base_mut(&mut self) -> Option<&mut Widget> { None }
fn preferred_height(&self) -> Option<f32> { None }
+ fn mark_dirty(&mut self, ctx: &mut UiContext) {
+ let mut parent_id = None;
+ if let Some(b) = self.base_mut() {
+ if b.dirty {
+ return;
+ }
+ b.dirty = true;
+ parent_id = b.id.get();
+ }
+ if let Some(id) = parent_id {
+ if let Some(&p_id) = ctx.layout_tree.parents.get(&id) {
+ if let Some(&parent_ptr) = ctx.widget_registry.get(&p_id) {
+ unsafe {
+ (*parent_ptr).mark_dirty(ctx);
+ }
+ }
+ }
+ }
+ }
+
fn as_any(&self) -> &dyn std::any::Any {
struct DummyAny;
static DUMMY: DummyAny = DummyAny;