GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
Implement dropdown upward opening, dynamic sizing, and section container fixes
src/color.rs | 4 +-
src/layout.rs | 91 ++++++++++++++++++++++++++++++-
src/widget/container/section_container.rs | 20 +++++--
src/widget/display/panel.rs | 5 ++
src/widget/input/button.rs | 11 +++-
src/widget/input/dropdown.rs | 28 ++++++----
src/widget/input/spinbox.rs | 7 +++
7 files changed, 146 insertions(+), 20 deletions(-)
diff --git a/src/color.rs b/src/color.rs
index a53375b..8c257f7 100644
--- a/src/color.rs
+++ b/src/color.rs
@@ -278,7 +278,7 @@ fn parse_and_set_colors(content: &str) {
if let Some(c) = get_color("/layout/color_borders_color") {
if let Ok(mut lock) = COLOR_BORDERS_COLOR.write() { *lock = c; }
}
- if let Some(c) = get_color("/layout/slider_track_color") {
+ if let Some(c) = get_color("/style/control/slider/color").or_else(|| get_color("/layout/slider_track_color")) {
if let Ok(mut lock) = SLIDER_TRACK_COLOR.write() { *lock = c; }
}
if let Some(c) = get_color("/layout/paginator_sidebar_color") {
@@ -308,7 +308,7 @@ fn parse_and_set_colors(content: &str) {
if let Some(c) = get_color("/layout/menubar_tab_label_color").or_else(|| get_color("/layout/paginator_tab_label_color")) {
if let Ok(mut lock) = MENUBAR_TAB_LABEL_COLOR.write() { *lock = c; }
}
- if let Some(c) = get_color("/layout/toggle_enabled_color") {
+ if let Some(c) = get_color("/style/control/toggle/enabled_color").or_else(|| get_color("/layout/toggle_enabled_color")) {
if let Ok(mut lock) = TOGGLE_ON_COLOR.write() { *lock = c; }
}
if let Some(c) = get_color("/style/control/toggle/disabled_color") {
diff --git a/src/layout.rs b/src/layout.rs
index 04b7f21..530bf14 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -135,9 +135,11 @@ fn flatten_json_to_flat_props(val: &serde_json::Value, prefix: &str, flat_props:
"style.surface.backplate.menubar.color" => "backplate_menubar_color",
"style.surface.backplate.menubar.text_color" => "backplate_menubar_text_color",
"style.surface.backplate.menubar.blur" => "backplate_menubar_blur",
+ "style.surface.backplate.menubar.font" => "menubar_font",
"style.surface.backplate.statusbar.color" => "backplate_statusbar_color",
"style.surface.backplate.statusbar.text_color" => "backplate_statusbar_text_color",
"style.surface.backplate.statusbar.blur" => "backplate_statusbar_blur",
+ "style.surface.backplate.statusbar.font" => "statusbar_font",
"style.surface.page.opacity" => "page_opacity",
"style.surface.page.margin" => "page_margin",
"style.surface.graph.cell_color" => "graph_cell_color",
@@ -232,6 +234,8 @@ static COLOR_SELECTOR_PREVIEW_MARGIN: RwLock<f32> = RwLock::new(0.0);
static COLOR_SELECTOR_CORNER_RADIUS: RwLock<f32> = RwLock::new(4.0);
static MENUBAR_FONT: RwLock<String> = RwLock::new(String::new());
static MENUBAR_FONT_CACHED: RwLock<Option<(String, f32)>> = RwLock::new(None);
+static STATUSBAR_FONT: RwLock<String> = RwLock::new(String::new());
+static STATUSBAR_FONT_CACHED: RwLock<Option<(String, f32)>> = RwLock::new(None);
static SECTION_LABEL_FONT: RwLock<String> = RwLock::new(String::new());
static NESTED_SECTION_LABEL_FONT: RwLock<String> = RwLock::new(String::new());
static BREADCRUMB_FONT: RwLock<String> = RwLock::new(String::new());
@@ -318,6 +322,7 @@ pub fn align_text_y(y: f32, height: f32, font_size: f32, top_offset: f32) -> f32
pub fn reload_config() {
if let Some(content) = read_config() {
let mut menubar_font_changed = false;
+ let mut statusbar_font_changed = false;
let mut toggle_font_changed = false;
let mut font_selector_font_changed = false;
let mut button_strip_font_changed = false;
@@ -649,6 +654,26 @@ pub fn reload_config() {
menubar_font_changed = true;
}
}
+ if let Some(rest) = trimmed.strip_prefix("statusbar_font") {
+ let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=');
+ let rest = rest.trim();
+ let val_str = if rest.starts_with('"') && rest.ends_with('"') && rest.len() >= 2 {
+ &rest[1..rest.len() - 1]
+ } else {
+ rest
+ };
+ let font = val_str.trim().to_string();
+ let mut changed = false;
+ if let Ok(mut lock) = STATUSBAR_FONT.write() {
+ if *lock != font {
+ *lock = font;
+ changed = true;
+ }
+ }
+ if changed {
+ statusbar_font_changed = true;
+ }
+ }
if let Some(rest) = trimmed.strip_prefix("section_label_font") {
let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=');
let rest = mod_rest(rest);
@@ -999,6 +1024,11 @@ pub fn reload_config() {
*lock = None;
}
}
+ if statusbar_font_changed {
+ if let Ok(mut lock) = STATUSBAR_FONT_CACHED.write() {
+ *lock = None;
+ }
+ }
if toggle_font_changed {
if let Ok(mut lock) = TOGGLE_FONT_CACHED.write() {
*lock = None;
@@ -1064,7 +1094,9 @@ pub fn reload_config() {
*lock = None;
}
}
- crate::color::reload_colors(&content);
+ if let Ok(raw_kdl) = std::fs::read_to_string(crate::config::get_config_path()) {
+ crate::color::reload_colors(&raw_kdl);
+ }
}
}
@@ -1671,6 +1703,63 @@ pub fn set_menubar_font(font: &str) {
}
}
+pub fn statusbar_font() -> String {
+ use std::sync::Once;
+ static INIT: Once = Once::new();
+ INIT.call_once(|| {
+ let mut font = "Outfit".to_string();
+ if let Some(content) = read_config() {
+ for line in content.lines() {
+ let trimmed = line.trim();
+ if let Some(rest) = trimmed.strip_prefix("statusbar_font") {
+ let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=');
+ let rest = rest.trim();
+ let val_str = if rest.starts_with('"') && rest.ends_with('"') && rest.len() >= 2 {
+ &rest[1..rest.len() - 1]
+ } else {
+ rest
+ };
+ font = val_str.trim().to_string();
+ }
+ }
+ }
+ if let Ok(mut lock) = STATUSBAR_FONT.write() {
+ *lock = font;
+ }
+ });
+ let lock = STATUSBAR_FONT.read().unwrap();
+ if lock.is_empty() {
+ "Outfit".to_string()
+ } else {
+ lock.clone()
+ }
+}
+
+pub fn statusbar_font_parsed() -> (String, f32) {
+ if let Ok(lock) = STATUSBAR_FONT_CACHED.read() {
+ if let Some(ref val) = *lock {
+ return val.clone();
+ }
+ }
+ let font_str = statusbar_font();
+ let parsed = parse_font_string(&font_str);
+ let size = parsed.1.unwrap_or(12.0);
+ let val = (parsed.0, size);
+ if let Ok(mut lock) = STATUSBAR_FONT_CACHED.write() {
+ *lock = Some(val.clone());
+ }
+ val
+}
+
+pub fn set_statusbar_font(font: &str) {
+ if let Ok(mut lock) = STATUSBAR_FONT.write() {
+ *lock = font.to_string();
+ }
+ if let Ok(mut lock) = STATUSBAR_FONT_CACHED.write() {
+ *lock = None;
+ }
+}
+
pub fn toggle_font() -> String {
use std::sync::Once;
static INIT: Once = Once::new();
diff --git a/src/widget/container/section_container.rs b/src/widget/container/section_container.rs
index 3b451de..f669b35 100644
--- a/src/widget/container/section_container.rs
+++ b/src/widget/container/section_container.rs
@@ -114,23 +114,33 @@ impl Element for SectionContainer {
}
fn all_quads(&self, ctx: &UiContext) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- self.container.all_quads(ctx)
+ let mut quads = self.header.all_quads(ctx);
+ quads.extend(self.container.all_quads(ctx));
+ quads
}
fn text_labels(&self) -> Vec<TextLabel> {
- self.container.text_labels()
+ 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]>)> {
- self.container.text_labels_with_bounds(ctx)
+ let mut labels = self.header.text_labels_with_bounds(ctx);
+ labels.extend(self.container.text_labels_with_bounds(ctx));
+ labels
}
fn text_labels_with_font_and_bounds(&self, ctx: &UiContext) -> Vec<(TextLabel, Option<String>, Option<[f32; 4]>)> {
- self.container.text_labels_with_font_and_bounds(ctx)
+ let mut labels = self.header.text_labels_with_font_and_bounds(ctx);
+ labels.extend(self.container.text_labels_with_font_and_bounds(ctx));
+ labels
}
fn get_text_items(&self) -> Vec<(&glyphon::Buffer, f32, f32, glyphon::Color)> {
- self.container.get_text_items()
+ let mut items = self.header.get_text_items();
+ items.extend(self.container.get_text_items());
+ items
}
fn hit_test(&self, px: f32, py: f32, ctx: &UiContext) -> bool {
diff --git a/src/widget/display/panel.rs b/src/widget/display/panel.rs
index aa6734b..9395457 100644
--- a/src/widget/display/panel.rs
+++ b/src/widget/display/panel.rs
@@ -38,6 +38,11 @@ impl Element for Panel {
fn color(&self) -> [f32; 4] { if self.dragging { colors::PANEL_DRAG } else { colors::PANEL_IDLE } }
+ fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+ let (x, y, w, h) = self.rect();
+ vec![(x, y, w, h, self.color())]
+ }
+
fn set_drag_bounds(&mut self, bx: f32, by: f32, bw: f32, bh: f32) {
self.bounds = Some((bx, by, bw, bh));
}
diff --git a/src/widget/input/button.rs b/src/widget/input/button.rs
index 1da0ab8..68cabc4 100644
--- a/src/widget/input/button.rs
+++ b/src/widget/input/button.rs
@@ -232,8 +232,15 @@ impl Element for Button {
}
let mut labels = Vec::new();
if let Some(ref label) = self.base.label {
- let font_size = 12.0;
- let font_family = self.widget_font().unwrap_or_else(|| "sans-serif".to_string());
+ let mut font_size = 12.0;
+ let mut font_family = "sans-serif".to_string();
+ if let Some(font_str) = self.widget_font() {
+ let (parsed_fam, parsed_size) = crate::layout::parse_font_string(&font_str);
+ font_family = parsed_fam;
+ if let Some(ps) = parsed_size {
+ font_size = ps;
+ }
+ }
let est_w = if label == "📋" {
12.0
} else {
diff --git a/src/widget/input/dropdown.rs b/src/widget/input/dropdown.rs
index ca561b5..b70ca7c 100644
--- a/src/widget/input/dropdown.rs
+++ b/src/widget/input/dropdown.rs
@@ -76,11 +76,21 @@ impl Dropdown {
w
}
+ pub fn get_dy_dh(&self) -> (f32, f32) {
+ let open_upward = self.base.y > 400.0;
+ let dh = self.options.len() as f32 * 24.0;
+ let dy = if open_upward {
+ self.base.y - dh
+ } else {
+ self.base.y + self.base.h
+ };
+ (dy, dh)
+ }
+
pub fn render_popover(&self, pc: &mut dyn crate::layout::RenderTarget) {
if !self.open { return; }
- let dy = self.base.y + self.base.h;
- let dh = self.options.len() as f32 * 24.0;
+ let (dy, dh) = self.get_dy_dh();
let pw = self.popover_width();
// 1. Soft layered drop shadows
@@ -258,8 +268,7 @@ impl Element for Dropdown {
let hx = if self.base.row_w > 0.0 { self.base.row_x } else { x };
let hw = if self.base.row_w > 0.0 { self.base.row_w } else { w };
if self.open {
- let dy = y + h;
- let dh = self.options.len() as f32 * 24.0;
+ let (dy, dh) = self.get_dy_dh();
let pw = self.popover_width();
let hit_trigger = px >= hx && px <= hx + hw && py >= y && py <= y + h;
let hit_popover = px >= x && px <= x + pw && py >= dy && py <= dy + dh;
@@ -277,9 +286,8 @@ impl Element for Dropdown {
self.hovered_item = None;
if self.open {
- let (x, y, _, h) = self.rect();
- let dy = y + h;
- let dh = self.options.len() as f32 * 24.0;
+ let (x, _, _, _) = self.rect();
+ let (dy, dh) = self.get_dy_dh();
let pw = self.popover_width();
if px >= x && px <= x + pw && py >= dy && py <= dy + dh {
let idx = ((py - dy) / 24.0) as usize;
@@ -304,8 +312,7 @@ impl Element for Dropdown {
if button != MouseButton::Left || state != ElementState::Pressed { return false; }
let (x, y, w, h) = self.rect();
- let dy = y + h;
- let dh = self.options.len() as f32 * 24.0;
+ let (dy, dh) = self.get_dy_dh();
let pw = self.popover_width();
let inside_trigger = px >= x && px <= x + w && py >= y && py <= y + h;
@@ -479,7 +486,8 @@ impl Element for Dropdown {
fn take_click(&mut self) -> bool { self.take_change() }
fn popover_rect(&self) -> Option<(f32, f32, f32, f32)> {
if self.open {
- Some((self.base.x, self.base.y + self.base.h, self.popover_width(), self.options.len() as f32 * 24.0))
+ let (dy, dh) = self.get_dy_dh();
+ Some((self.base.x, dy, self.popover_width(), dh))
} else {
None
}
diff --git a/src/widget/input/spinbox.rs b/src/widget/input/spinbox.rs
index 99c4958..52d7bdf 100644
--- a/src/widget/input/spinbox.rs
+++ b/src/widget/input/spinbox.rs
@@ -78,6 +78,13 @@ impl Spinbox {
impl Element for Spinbox {
crate::impl_widget_base!(Spinbox);
+ 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.base.label_offset();
+ }
+
fn get_value_string(&self) -> Option<String> {
if self.decimals > 0 {
let divisor = 10.0f32.powi(self.decimals as i32);