GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
Unify ColumnarList and List into a single List widget with dynamic columns support
src/widget/container/columnar_list.rs | 519 ---------------------------------
src/widget/container/mod.rs | 4 +-
src/widget/container/scrolling_list.rs | 440 ++++++++++++++++++++++++++--
src/widget/mod.rs | 8 +-
4 files changed, 420 insertions(+), 551 deletions(-)
diff --git a/src/widget/container/columnar_list.rs b/src/widget/container/columnar_list.rs
deleted file mode 100644
index a0d4a45..0000000
--- a/src/widget/container/columnar_list.rs
+++ /dev/null
@@ -1,519 +0,0 @@
-use crate::widget::*;
-use super::scroll_box::ScrollBox;
-use crate::widget::display::TextLabel;
-use crate::context::UiContext;
-
-#[derive(Debug, Clone, Copy, PartialEq)]
-pub enum ColumnWidth {
- Flex,
- Absolute(f32),
- RightOffset(f32),
-}
-
-#[derive(Debug, Clone)]
-pub struct ListColumn {
- pub name: String,
- pub width: ColumnWidth,
- pub justification: Justification,
-}
-
-#[derive(Debug, Clone)]
-pub struct ListRow {
- pub cells: Vec<String>,
- pub icon: Option<String>,
- pub selected: bool,
-}
-
-#[derive(Debug, Clone)]
-pub struct ColumnarList {
- pub base: Widget,
- pub scroll_box: ScrollBox,
- pub columns: Vec<ListColumn>,
- pub rows: Vec<ListRow>,
- pub row_height: f32,
- pub row_gap: f32,
- pub hovered_row: Option<usize>,
- pub pressed_row: Option<usize>,
- pub clicked_row: Option<usize>,
- pub double_clicked_row: Option<usize>,
- pub last_click_time: Option<std::time::Instant>,
-}
-
-impl ColumnarList {
- pub fn new(columns: Vec<ListColumn>, row_height: f32, row_gap: f32) -> Self {
- Self {
- base: Widget::new(),
- scroll_box: ScrollBox::new(),
- columns,
- rows: Vec::new(),
- row_height,
- row_gap,
- hovered_row: None,
- pressed_row: None,
- clicked_row: None,
- double_clicked_row: None,
- last_click_time: None,
- }
- }
-
- pub fn get_column_bounds(&self, list_w: f32) -> Vec<(f32, f32)> {
- let mut bounds = vec![(0.0, 0.0); self.columns.len()];
- let mut flex_indices = Vec::new();
- let mut reserved_width = 0.0;
-
- // Pass 1: Resolve Absolute and RightOffset columns
- for (i, col) in self.columns.iter().enumerate() {
- match col.width {
- ColumnWidth::Absolute(w) => {
- bounds[i] = (0.0, w);
- reserved_width += w;
- }
- ColumnWidth::RightOffset(offset) => {
- let x = list_w - offset;
- let mut next_x = list_w;
- for j in (i + 1)..self.columns.len() {
- if let ColumnWidth::RightOffset(o) = self.columns[j].width {
- next_x = list_w - o;
- break;
- }
- }
- let w = (next_x - x).max(0.0);
- bounds[i] = (x, w);
- }
- ColumnWidth::Flex => {
- flex_indices.push(i);
- }
- }
- }
-
- // Pass 2: Layout left-aligned columns (Flex and Absolute)
- let mut current_x = 0.0;
- let mut right_boundary = list_w;
- for (_, col) in self.columns.iter().enumerate() {
- if let ColumnWidth::RightOffset(offset) = col.width {
- if list_w - offset < right_boundary {
- right_boundary = list_w - offset;
- }
- }
- }
-
- let left_space = (right_boundary - current_x).max(0.0);
- let flex_share = if !flex_indices.is_empty() {
- let flex_total = (left_space - reserved_width).max(0.0);
- flex_total / flex_indices.len() as f32
- } else {
- 0.0
- };
-
- for (i, col) in self.columns.iter().enumerate() {
- match col.width {
- ColumnWidth::Absolute(w) => {
- bounds[i] = (current_x, w);
- current_x += w;
- }
- ColumnWidth::Flex => {
- bounds[i] = (current_x, flex_share);
- current_x += flex_share;
- }
- ColumnWidth::RightOffset(_) => {
- // Already resolved
- }
- }
- }
-
- bounds
- }
-
- pub fn take_click(&mut self) -> Option<usize> {
- self.clicked_row.take()
- }
-
- pub fn take_double_click(&mut self) -> Option<usize> {
- self.double_clicked_row.take()
- }
-
- pub fn update_bounds(&mut self) {
- let item_height_full = self.row_height + self.row_gap;
- let content_h = self.rows.len() as f32 * item_height_full + 4.0;
- let (_, _, _, h) = self.rect();
- self.scroll_box.update_bounds(content_h, self.scroll_box.viewport_y, h);
- }
-}
-
-impl Element for ColumnarList {
- fn base(&self) -> Option<&Widget> { Some(&self.base) }
- fn base_mut(&mut self) -> Option<&mut Widget> { Some(&mut self.base) }
-
- fn rect(&self) -> (f32, f32, f32, f32) {
- self.scroll_box.rect()
- }
-
- 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.scroll_box.set_rect(x, y, w, h);
- self.update_bounds();
- }
-
- fn color(&self) -> [f32; 4] {
- self.scroll_box.color()
- }
-
- fn rounded_corners(&self) -> (bool, bool, bool, bool) {
- (true, true, true, true)
- }
-
- fn corner_radius(&self) -> f32 {
- crate::layout::list_corner_radius()
- }
-
- fn solid_border(&self) -> Option<([f32; 4], f32)> {
- let is_focused = focus::is_focused(self) || focus::is_focused(&self.scroll_box);
- let is_hovered = self.hovered() || self.scroll_box.base.hovered;
- if self.scroll_box.show_border {
- let box_border_color = if is_focused {
- [0.30, 0.50, 0.32, 1.0]
- } else if is_hovered {
- [0.25, 0.25, 0.35, 1.0]
- } else {
- [0.18, 0.18, 0.24, 1.0]
- };
- Some((box_border_color, 1.0))
- } else {
- None
- }
- }
-
- 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_hovered(&mut self, v: bool) {
- self.scroll_box.set_hovered(v);
- if !v {
- self.hovered_row = None;
- }
- }
-
- fn hovered(&self) -> bool {
- self.scroll_box.hovered()
- }
-
- fn highlight_color(&self, ctx: &UiContext) -> Option<[f32; 4]> {
- self.scroll_box.highlight_color(ctx)
- }
-
- fn on_cursor_moved(&mut self, px: f32, py: f32, ctx: &mut UiContext) -> bool {
- self.scroll_box.cursor_moved(px, py, ctx);
- let (x, _, w, _) = self.rect();
- let item_height_full = self.row_height + self.row_gap;
-
- let mut new_hovered = None;
- for idx in 0..self.rows.len() {
- let virtual_y = idx as f32 * item_height_full + 2.0;
- if let Some(draw_y) = self.scroll_box.get_item_draw_y(virtual_y, self.row_height) {
- if px >= x + 2.0 && px <= x + w - 2.0 && py >= draw_y && py <= draw_y + self.row_height {
- new_hovered = Some(idx);
- break;
- }
- }
- }
-
- let changed = self.hovered_row != new_hovered;
- self.hovered_row = new_hovered;
- changed
- }
-
- fn mouse_wheel(&mut self, delta: &MouseScrollDelta, px: f32, py: f32, ctx: &mut UiContext) -> bool {
- self.scroll_box.mouse_wheel(delta, px, py, ctx)
- }
-
- fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32, ctx: &mut UiContext) -> bool {
- let (x, _, w, _) = self.rect();
- let was_scroll = self.scroll_box.mouse_input(button, state, px, py, ctx);
-
- if button != MouseButton::Left {
- return was_scroll;
- }
-
- let item_height_full = self.row_height + self.row_gap;
- let mut clicked_idx = None;
- for idx in 0..self.rows.len() {
- let virtual_y = idx as f32 * item_height_full + 2.0;
- if let Some(draw_y) = self.scroll_box.get_item_draw_y(virtual_y, self.row_height) {
- if px >= x + 2.0 && px <= x + w - 2.0 && py >= draw_y && py <= draw_y + self.row_height {
- clicked_idx = Some(idx);
- break;
- }
- }
- }
-
- match state {
- ElementState::Pressed => {
- if let Some(idx) = clicked_idx {
- self.pressed_row = Some(idx);
- self.scroll_box.focus();
- ctx.set_focused_ptr(self.as_ptr_mut());
- return true;
- }
- }
- ElementState::Released => {
- let mut clicked = false;
- if let Some(idx) = clicked_idx {
- if self.pressed_row == Some(idx) {
- self.scroll_box.focus();
- let now = std::time::Instant::now();
- if let Some(last) = self.last_click_time {
- if now.duration_since(last) < std::time::Duration::from_millis(400) {
- self.double_clicked_row = Some(idx);
- }
- }
- self.last_click_time = Some(now);
- self.clicked_row = Some(idx);
- clicked = true;
- }
- }
- self.pressed_row = None;
- return clicked || was_scroll;
- }
- }
- was_scroll
- }
-
- fn focus(&mut self) {
- self.scroll_box.focus();
- }
-
- fn unfocus(&mut self) {
- self.scroll_box.unfocus();
- }
-
- fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- let mut quads = self.scroll_box.extra_quads();
- let (x, _, w, _) = self.rect();
- let item_height_full = self.row_height + self.row_gap;
-
- let (r1, r2, r3, r4) = self.rounded_corners();
- if r1 || r2 || r3 || r4 {
- if !quads.is_empty() {
- quads.remove(0);
- }
- }
-
- for idx in 0..self.rows.len() {
- let virtual_y = idx as f32 * item_height_full + 2.0;
- if let Some(draw_y) = self.scroll_box.get_item_draw_y(virtual_y, self.row_height) {
- let row = &self.rows[idx];
- let is_hovered = self.hovered_row == Some(idx);
- let is_pressed = self.pressed_row == Some(idx);
-
- let bg_color = if row.selected {
- if is_pressed { [0.30, 0.52, 0.78, 0.6] }
- else if is_hovered { [0.30, 0.52, 0.78, 0.5] }
- else { [0.20, 0.40, 0.65, 0.4] }
- } else {
- if is_pressed { [0.20, 0.20, 0.25, 0.25] }
- else if is_hovered { [0.20, 0.20, 0.25, 0.15] }
- else { [0.0, 0.0, 0.0, 0.0] }
- };
-
- if bg_color[3] > 0.001 {
- quads.push((x + 2.0, draw_y, w - 4.0, self.row_height, bg_color));
- }
- }
- }
- quads
- }
-
- fn all_rounded_quads(&self, ctx: &UiContext) -> Vec<(f32, f32, f32, f32, f32, [f32; 4], (bool, bool, bool, bool))> {
- let mut quads = Vec::new();
- let (r1, r2, r3, r4) = self.rounded_corners();
- let has_rounded = r1 || r2 || r3 || r4;
- if !has_rounded {
- for &child_ptr in &self.children(ctx) {
- let widget = unsafe { &*child_ptr };
- quads.extend(widget.all_rounded_quads(ctx));
- }
- return quads;
- }
-
- let radius = self.corner_radius();
- let (x, y, w, h) = self.rect();
-
- if let Some((border_color, thickness)) = self.solid_border() {
- quads.push((x, y, w, h, radius, border_color, (r1, r2, r3, r4)));
- quads.push((x + thickness, y + thickness, w - 2.0 * thickness, h - 2.0 * thickness, radius - thickness, crate::color::list_bg_color(), (r1, r2, r3, r4)));
- } else {
- quads.push((x, y, w, h, radius, crate::color::list_bg_color(), (r1, r2, r3, r4)));
- }
-
- for &child_ptr in &self.children(ctx) {
- let widget = unsafe { &*child_ptr };
- quads.extend(widget.all_rounded_quads(ctx));
- }
- quads
- }
-
- fn text_labels_with_font_and_bounds(&self, _ctx: &UiContext) -> Vec<(TextLabel, Option<String>, Option<[f32; 4]>)> {
- if !self.visible() {
- return Vec::new();
- }
- let (x, _, w, _) = self.rect();
- let mut result = Vec::new();
-
- let item_height_full = self.row_height + self.row_gap;
- let col_bounds = self.get_column_bounds(w);
-
- let fg = [230, 230, 242];
- let text_dim = [140, 140, 153];
-
- let view_min = self.scroll_box.viewport_y + 4.0;
- let view_max = self.scroll_box.viewport_y + self.scroll_box.viewport_h - 4.0;
- let clipping_bounds = Some([x, view_min, x + w, view_max]);
-
- for idx in 0..self.rows.len() {
- let virtual_y = idx as f32 * item_height_full + 2.0;
- if let Some(draw_y) = self.scroll_box.get_item_draw_y(virtual_y, self.row_height) {
- let row = &self.rows[idx];
- let row_fg = if row.selected { fg } else { [178, 178, 191] };
- let row_dim = if row.selected { fg } else { text_dim };
-
- let y_text_12 = crate::layout::center_text_y(draw_y, self.row_height, 12.0);
- let y_text_11 = crate::layout::center_text_y(draw_y, self.row_height, 11.0);
-
- let mut start_text_offset = 8.0;
- if let Some(ref icon) = row.icon {
- if !col_bounds.is_empty() {
- result.push((
- TextLabel {
- text: icon.clone(),
- x: x + col_bounds[0].0 + 12.0,
- y: y_text_12,
- font_size: 12.0,
- color: row_fg,
- },
- None,
- clipping_bounds,
- ));
- start_text_offset = 32.0;
- }
- }
-
- for (c_idx, cell_text) in row.cells.iter().enumerate() {
- if c_idx >= col_bounds.len() {
- break;
- }
- let (col_x, col_w) = col_bounds[c_idx];
- if col_w <= 0.0 {
- continue;
- }
-
- let cell_color = if c_idx == 0 { row_fg } else { row_dim };
- let cell_y = if c_idx == 0 { y_text_12 } else { y_text_11 };
- let cell_size = if c_idx == 0 { 12.0 } else { 11.0 };
-
- let cell_draw_x = if c_idx == 0 {
- x + col_x + start_text_offset
- } else {
- x + col_x
- };
-
- let max_w = if c_idx == 0 {
- col_w - start_text_offset - 8.0
- } else {
- col_w - 8.0
- };
- let char_w = cell_size * 0.65;
- let max_chars = (max_w / char_w).max(4.0) as usize;
- let cell_text_truncated = if cell_text.chars().count() > max_chars {
- let mut s: String = cell_text.chars().take(max_chars - 3).collect();
- s.push_str("...");
- s
- } else {
- cell_text.clone()
- };
-
- result.push((
- TextLabel {
- text: cell_text_truncated,
- x: cell_draw_x,
- y: cell_y,
- font_size: cell_size,
- color: cell_color,
- },
- None,
- clipping_bounds,
- ));
- }
- }
- }
- result
- }
-
- fn keyboard_input(&mut self, event: &KeyEvent, ctx: &mut UiContext) -> bool {
- if self.scroll_box.keyboard_input(event, ctx) {
- return true;
- }
- if event.state == ElementState::Pressed {
- let mut current_selected = None;
- for (idx, r) in self.rows.iter().enumerate() {
- if r.selected {
- current_selected = Some(idx);
- break;
- }
- }
-
- let mut next_selected = None;
- if event.logical_key == Key::Named(NamedKey::ArrowDown) {
- if let Some(curr) = current_selected {
- if curr + 1 < self.rows.len() {
- next_selected = Some(curr + 1);
- }
- } else if !self.rows.is_empty() {
- next_selected = Some(0);
- }
- } else if event.logical_key == Key::Named(NamedKey::ArrowUp) {
- if let Some(curr) = current_selected {
- if curr > 0 {
- next_selected = Some(curr - 1);
- }
- }
- }
-
- if let Some(next) = next_selected {
- for (idx, r) in self.rows.iter_mut().enumerate() {
- r.selected = idx == next;
- }
- self.clicked_row = Some(next);
-
- let item_height_full = self.row_height + self.row_gap;
- let item_y = next as f32 * item_height_full + 2.0;
- let viewport_h = self.scroll_box.viewport_h;
-
- if item_y < self.scroll_box.scroll_y {
- self.scroll_box.scroll_y = item_y;
- } else if item_y + self.row_height > self.scroll_box.scroll_y + viewport_h {
- self.scroll_box.scroll_y = item_y + self.row_height - viewport_h;
- }
-
- self.mark_dirty(ctx);
- return true;
- }
- }
- false
- }
-
- fn parent(&self, _ctx: &UiContext) -> Option<*mut (dyn Element + 'static)> { self.scroll_box.parent(_ctx) }
- fn set_parent(&mut self, parent: Option<*mut (dyn Element + 'static)>, ctx: &mut UiContext) { self.scroll_box.set_parent(parent, ctx); }
- fn children(&self, ctx: &UiContext) -> Vec<*mut (dyn Element + 'static)> { self.scroll_box.children(ctx) }
- fn add_child(&mut self, child: *mut (dyn Element + 'static), ctx: &mut UiContext) { self.scroll_box.add_child(child, ctx); }
- fn clear_children(&mut self, ctx: &mut UiContext) { self.scroll_box.clear_children(ctx); }
-}
-
-unsafe impl Send for ColumnarList {}
-unsafe impl Sync for ColumnarList {}
diff --git a/src/widget/container/mod.rs b/src/widget/container/mod.rs
index 1d8cb55..6d3eb0f 100644
--- a/src/widget/container/mod.rs
+++ b/src/widget/container/mod.rs
@@ -9,7 +9,6 @@ pub mod breadcrumb;
pub mod spreadsheet;
pub mod scroll_box;
pub mod scrolling_list;
-pub mod columnar_list;
pub mod plate;
pub mod switcher;
pub mod layer;
@@ -33,8 +32,7 @@ pub use menu::{Menu, MenuBar};
pub use breadcrumb::Breadcrumb;
pub use spreadsheet::Spreadsheet;
pub use scroll_box::ScrollBox;
-pub use scrolling_list::{List, FontSelector};
-pub use columnar_list::{ColumnarList, ListColumn, ListRow, ColumnWidth};
+pub use scrolling_list::{List, FontSelector, ListColumn, ListRow, ColumnWidth};
pub use plate::Plate;
pub use switcher::Switcher;
pub use layer::Layer;
diff --git a/src/widget/container/scrolling_list.rs b/src/widget/container/scrolling_list.rs
index 1b57743..7e96261 100644
--- a/src/widget/container/scrolling_list.rs
+++ b/src/widget/container/scrolling_list.rs
@@ -1,31 +1,160 @@
use crate::widget::*;
use super::scroll_box::ScrollBox;
+use crate::widget::display::TextLabel;
+use crate::context::UiContext;
pub use crate::widget::input::font_selector::FontSelector;
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum ColumnWidth {
+ Flex,
+ Absolute(f32),
+ RightOffset(f32),
+}
+
+#[derive(Debug, Clone)]
+pub struct ListColumn {
+ pub name: String,
+ pub width: ColumnWidth,
+ pub justification: Justification,
+}
+
+#[derive(Debug, Clone)]
+pub struct ListRow {
+ pub cells: Vec<String>,
+ pub icon: Option<String>,
+ pub selected: bool,
+}
-// Generic text item layout wrapper
#[derive(Debug, Clone)]
pub struct List {
+ pub base: Widget,
pub scroll_box: ScrollBox,
pub item_height: f32,
pub item_gap: f32,
+ pub columns: Option<Vec<ListColumn>>,
+ pub rows: Vec<ListRow>,
+ pub hovered_row: Option<usize>,
+ pub pressed_row: Option<usize>,
+ pub clicked_row: Option<usize>,
+ pub double_clicked_row: Option<usize>,
+ pub last_click_time: Option<std::time::Instant>,
}
impl List {
pub fn new(item_height: f32, item_gap: f32) -> Self {
Self {
+ base: Widget::new(),
scroll_box: ScrollBox::new(),
item_height,
item_gap,
+ columns: None,
+ rows: Vec::new(),
+ hovered_row: None,
+ pressed_row: None,
+ clicked_row: None,
+ double_clicked_row: None,
+ last_click_time: None,
}
}
+ pub fn with_columns(mut self, columns: Vec<ListColumn>) -> Self {
+ self.columns = Some(columns);
+ self
+ }
+
+ pub fn get_column_bounds(&self, list_w: f32) -> Vec<(f32, f32)> {
+ let cols = match &self.columns {
+ Some(c) => c,
+ None => return Vec::new(),
+ };
+ let mut bounds = vec![(0.0, 0.0); cols.len()];
+ let mut flex_indices = Vec::new();
+ let mut reserved_width = 0.0;
+
+ // Pass 1: Resolve Absolute and RightOffset columns
+ for (i, col) in cols.iter().enumerate() {
+ match col.width {
+ ColumnWidth::Absolute(w) => {
+ bounds[i] = (0.0, w);
+ reserved_width += w;
+ }
+ ColumnWidth::RightOffset(offset) => {
+ let x = list_w - offset;
+ let mut next_x = list_w;
+ for j in (i + 1)..cols.len() {
+ if let ColumnWidth::RightOffset(o) = cols[j].width {
+ next_x = list_w - o;
+ break;
+ }
+ }
+ let w = (next_x - x).max(0.0);
+ bounds[i] = (x, w);
+ }
+ ColumnWidth::Flex => {
+ flex_indices.push(i);
+ }
+ }
+ }
+
+ // Pass 2: Layout left-aligned columns (Flex and Absolute)
+ let mut current_x = 0.0;
+ let mut right_boundary = list_w;
+ for (_, col) in cols.iter().enumerate() {
+ if let ColumnWidth::RightOffset(offset) = col.width {
+ if list_w - offset < right_boundary {
+ right_boundary = list_w - offset;
+ }
+ }
+ }
+
+ let left_space = (right_boundary - current_x).max(0.0);
+ let flex_share = if !flex_indices.is_empty() {
+ let flex_total = (left_space - reserved_width).max(0.0);
+ flex_total / flex_indices.len() as f32
+ } else {
+ 0.0
+ };
+
+ for (i, col) in cols.iter().enumerate() {
+ match col.width {
+ ColumnWidth::Absolute(w) => {
+ bounds[i] = (current_x, w);
+ current_x += w;
+ }
+ ColumnWidth::Flex => {
+ bounds[i] = (current_x, flex_share);
+ current_x += flex_share;
+ }
+ ColumnWidth::RightOffset(_) => {
+ // Already resolved
+ }
+ }
+ }
+
+ bounds
+ }
+
+ pub fn take_click(&mut self) -> Option<usize> {
+ self.clicked_row.take()
+ }
+
+ pub fn take_double_click(&mut self) -> Option<usize> {
+ self.double_clicked_row.take()
+ }
+
pub fn update_bounds(&mut self, count: usize, viewport_y: f32, viewport_h: f32) {
let item_height_full = self.item_height + self.item_gap;
- let content_h = count as f32 * item_height_full;
+ let content_h = count as f32 * item_height_full + 4.0;
self.scroll_box.update_bounds(content_h, viewport_y, viewport_h);
}
+ pub fn update_bounds_from_rows(&mut self) {
+ let item_height_full = self.item_height + self.item_gap;
+ let content_h = self.rows.len() as f32 * item_height_full + 4.0;
+ let (_, _, _, h) = self.rect();
+ self.scroll_box.update_bounds(content_h, self.scroll_box.viewport_y, h);
+ }
+
pub fn get_item_draw_y(&self, idx: usize, offset: f32) -> Option<f32> {
let item_height_full = self.item_height + self.item_gap;
let virtual_y = idx as f32 * item_height_full + offset;
@@ -48,12 +177,22 @@ impl Default for List {
}
impl Element for List {
+ fn base(&self) -> Option<&Widget> { Some(&self.base) }
+ fn base_mut(&mut self) -> Option<&mut Widget> { Some(&mut self.base) }
+
fn rect(&self) -> (f32, f32, f32, f32) {
self.scroll_box.rect()
}
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.scroll_box.set_rect(x, y, w, h);
+ if self.columns.is_some() && !self.rows.is_empty() {
+ self.update_bounds_from_rows();
+ }
}
fn color(&self) -> [f32; 4] {
@@ -84,6 +223,7 @@ impl Element for List {
None
}
}
+
fn as_ptr(&self) -> *mut (dyn Element + 'static) {
self as *const Self as *mut Self as *mut (dyn Element + 'static)
}
@@ -93,6 +233,9 @@ impl Element for List {
fn set_hovered(&mut self, v: bool) {
self.scroll_box.set_hovered(v);
+ if !v {
+ self.hovered_row = None;
+ }
}
fn hovered(&self) -> bool {
@@ -104,7 +247,27 @@ impl Element for List {
}
fn on_cursor_moved(&mut self, px: f32, py: f32, ctx: &mut UiContext) -> bool {
- self.scroll_box.cursor_moved(px, py, ctx)
+ self.scroll_box.cursor_moved(px, py, ctx);
+ if self.columns.is_none() {
+ return false;
+ }
+ let (x, _, w, _) = self.rect();
+ let item_height_full = self.item_height + self.item_gap;
+
+ let mut new_hovered = None;
+ for idx in 0..self.rows.len() {
+ let virtual_y = idx as f32 * item_height_full + 2.0;
+ if let Some(draw_y) = self.scroll_box.get_item_draw_y(virtual_y, self.item_height) {
+ if px >= x + 2.0 && px <= x + w - 2.0 && py >= draw_y && py <= draw_y + self.item_height {
+ new_hovered = Some(idx);
+ break;
+ }
+ }
+ }
+
+ let changed = self.hovered_row != new_hovered;
+ self.hovered_row = new_hovered;
+ changed
}
fn mouse_wheel(&mut self, delta: &MouseScrollDelta, px: f32, py: f32, ctx: &mut UiContext) -> bool {
@@ -112,7 +275,55 @@ impl Element for List {
}
fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32, ctx: &mut UiContext) -> bool {
- self.scroll_box.mouse_input(button, state, px, py, ctx)
+ let (x, _, w, _) = self.rect();
+ let was_scroll = self.scroll_box.mouse_input(button, state, px, py, ctx);
+
+ if self.columns.is_none() || button != MouseButton::Left {
+ return was_scroll;
+ }
+
+ let item_height_full = self.item_height + self.item_gap;
+ let mut clicked_idx = None;
+ for idx in 0..self.rows.len() {
+ let virtual_y = idx as f32 * item_height_full + 2.0;
+ if let Some(draw_y) = self.scroll_box.get_item_draw_y(virtual_y, self.item_height) {
+ if px >= x + 2.0 && px <= x + w - 2.0 && py >= draw_y && py <= draw_y + self.item_height {
+ clicked_idx = Some(idx);
+ break;
+ }
+ }
+ }
+
+ match state {
+ ElementState::Pressed => {
+ if let Some(idx) = clicked_idx {
+ self.pressed_row = Some(idx);
+ self.scroll_box.focus();
+ ctx.set_focused_ptr(self.as_ptr_mut());
+ return true;
+ }
+ }
+ ElementState::Released => {
+ let mut clicked = false;
+ if let Some(idx) = clicked_idx {
+ if self.pressed_row == Some(idx) {
+ self.scroll_box.focus();
+ let now = std::time::Instant::now();
+ if let Some(last) = self.last_click_time {
+ if now.duration_since(last) < std::time::Duration::from_millis(400) {
+ self.double_clicked_row = Some(idx);
+ }
+ }
+ self.last_click_time = Some(now);
+ self.clicked_row = Some(idx);
+ clicked = true;
+ }
+ }
+ self.pressed_row = None;
+ return clicked || was_scroll;
+ }
+ }
+ was_scroll
}
fn focus(&mut self) {
@@ -125,13 +336,41 @@ impl Element for List {
fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
let mut quads = self.scroll_box.extra_quads();
+ if self.columns.is_none() {
+ return quads;
+ }
+ let (x, _, w, _) = self.rect();
+ let item_height_full = self.item_height + self.item_gap;
+
let (r1, r2, r3, r4) = self.rounded_corners();
- let has_rounded = r1 || r2 || r3 || r4;
- if has_rounded {
+ if r1 || r2 || r3 || r4 {
if !quads.is_empty() {
quads.remove(0);
}
}
+
+ for idx in 0..self.rows.len() {
+ let virtual_y = idx as f32 * item_height_full + 2.0;
+ if let Some(draw_y) = self.scroll_box.get_item_draw_y(virtual_y, self.item_height) {
+ let row = &self.rows[idx];
+ let is_hovered = self.hovered_row == Some(idx);
+ let is_pressed = self.pressed_row == Some(idx);
+
+ let bg_color = if row.selected {
+ if is_pressed { [0.30, 0.52, 0.78, 0.6] }
+ else if is_hovered { [0.30, 0.52, 0.78, 0.5] }
+ else { [0.20, 0.40, 0.65, 0.4] }
+ } else {
+ if is_pressed { [0.20, 0.20, 0.25, 0.25] }
+ else if is_hovered { [0.20, 0.20, 0.25, 0.15] }
+ else { [0.0, 0.0, 0.0, 0.0] }
+ };
+
+ if bg_color[3] > 0.001 {
+ quads.push((x + 2.0, draw_y, w - 4.0, self.item_height, bg_color));
+ }
+ }
+ }
quads
}
@@ -150,7 +389,6 @@ impl Element for List {
let radius = self.corner_radius();
let (x, y, w, h) = self.rect();
- // Solid border if active
if let Some((border_color, thickness)) = self.solid_border() {
quads.push((x, y, w, h, radius, border_color, (r1, r2, r3, r4)));
quads.push((x + thickness, y + thickness, w - 2.0 * thickness, h - 2.0 * thickness, radius - thickness, crate::color::list_bg_color(), (r1, r2, r3, r4)));
@@ -165,28 +403,186 @@ impl Element for List {
quads
}
+ fn text_labels_with_font_and_bounds(&self, ctx: &UiContext) -> Vec<(TextLabel, Option<String>, Option<[f32; 4]>)> {
+ if !self.visible() {
+ return Vec::new();
+ }
+ if self.columns.is_none() {
+ let font = self.widget_font();
+ let mut labels = self.text_labels().into_iter().map(|l| (l, font.clone(), None::<[f32; 4]>)).collect::<Vec<_>>();
+ let mut curr = self.parent(ctx);
+ let mut scroll_box_bounds = None;
+ while let Some(parent_ptr) = curr {
+ let parent = unsafe { &*parent_ptr };
+ if let Some(scroll_box) = parent.as_any().downcast_ref::<crate::widget::ScrollBox>() {
+ let (sb_x, _, sb_w, _) = scroll_box.rect();
+ let view_min = scroll_box.viewport_y + 4.0;
+ let view_max = scroll_box.viewport_y + scroll_box.viewport_h - 4.0;
+ scroll_box_bounds = Some([sb_x, view_min, sb_x + sb_w, view_max]);
+ break;
+ }
+ curr = parent.parent(ctx);
+ }
+ if let Some(sb_bounds) = scroll_box_bounds {
+ for item in &mut labels {
+ item.2 = Some(sb_bounds);
+ }
+ }
+ return labels;
+ }
+ let (x, _, w, _) = self.rect();
+ let mut result = Vec::new();
+
+ let item_height_full = self.item_height + self.item_gap;
+ let col_bounds = self.get_column_bounds(w);
+
+ let fg = [230, 230, 242];
+ let text_dim = [140, 140, 153];
+
+ let view_min = self.scroll_box.viewport_y + 4.0;
+ let view_max = self.scroll_box.viewport_y + self.scroll_box.viewport_h - 4.0;
+ let clipping_bounds = Some([x, view_min, x + w, view_max]);
+
+ for idx in 0..self.rows.len() {
+ let virtual_y = idx as f32 * item_height_full + 2.0;
+ if let Some(draw_y) = self.scroll_box.get_item_draw_y(virtual_y, self.item_height) {
+ let row = &self.rows[idx];
+ let row_fg = if row.selected { fg } else { [178, 178, 191] };
+ let row_dim = if row.selected { fg } else { text_dim };
+
+ let y_text_12 = crate::layout::center_text_y(draw_y, self.item_height, 12.0);
+ let y_text_11 = crate::layout::center_text_y(draw_y, self.item_height, 11.0);
+
+ let mut start_text_offset = 8.0;
+ if let Some(ref icon) = row.icon {
+ if !col_bounds.is_empty() {
+ result.push((
+ TextLabel {
+ text: icon.clone(),
+ x: x + col_bounds[0].0 + 12.0,
+ y: y_text_12,
+ font_size: 12.0,
+ color: row_fg,
+ },
+ None,
+ clipping_bounds,
+ ));
+ start_text_offset = 32.0;
+ }
+ }
+
+ for (c_idx, cell_text) in row.cells.iter().enumerate() {
+ if c_idx >= col_bounds.len() {
+ break;
+ }
+ let (col_x, col_w) = col_bounds[c_idx];
+ if col_w <= 0.0 {
+ continue;
+ }
+
+ let cell_color = if c_idx == 0 { row_fg } else { row_dim };
+ let cell_y = if c_idx == 0 { y_text_12 } else { y_text_11 };
+ let cell_size = if c_idx == 0 { 12.0 } else { 11.0 };
+
+ let cell_draw_x = if c_idx == 0 {
+ x + col_x + start_text_offset
+ } else {
+ x + col_x
+ };
+
+ let max_w = if c_idx == 0 {
+ col_w - start_text_offset - 8.0
+ } else {
+ col_w - 8.0
+ };
+ let char_w = cell_size * 0.65;
+ let max_chars = (max_w / char_w).max(4.0) as usize;
+ let cell_text_truncated = if cell_text.chars().count() > max_chars {
+ let mut s: String = cell_text.chars().take(max_chars - 3).collect();
+ s.push_str("...");
+ s
+ } else {
+ cell_text.clone()
+ };
+
+ result.push((
+ TextLabel {
+ text: cell_text_truncated,
+ x: cell_draw_x,
+ y: cell_y,
+ font_size: cell_size,
+ color: cell_color,
+ },
+ None,
+ clipping_bounds,
+ ));
+ }
+ }
+ }
+ result
+ }
+
fn keyboard_input(&mut self, event: &KeyEvent, ctx: &mut UiContext) -> bool {
- self.scroll_box.keyboard_input(event, ctx)
+ if self.scroll_box.keyboard_input(event, ctx) {
+ return true;
+ }
+ if self.columns.is_none() {
+ return false;
+ }
+ if event.state == ElementState::Pressed {
+ let mut current_selected = None;
+ for (idx, r) in self.rows.iter().enumerate() {
+ if r.selected {
+ current_selected = Some(idx);
+ break;
+ }
+ }
+
+ let mut next_selected = None;
+ if event.logical_key == Key::Named(NamedKey::ArrowDown) {
+ if let Some(curr) = current_selected {
+ if curr + 1 < self.rows.len() {
+ next_selected = Some(curr + 1);
+ }
+ } else if !self.rows.is_empty() {
+ next_selected = Some(0);
+ }
+ } else if event.logical_key == Key::Named(NamedKey::ArrowUp) {
+ if let Some(curr) = current_selected {
+ if curr > 0 {
+ next_selected = Some(curr - 1);
+ }
+ }
+ }
+
+ if let Some(next) = next_selected {
+ for (idx, r) in self.rows.iter_mut().enumerate() {
+ r.selected = idx == next;
+ }
+ self.clicked_row = Some(next);
+
+ let item_height_full = self.item_height + self.item_gap;
+ let item_y = next as f32 * item_height_full + 2.0;
+ let viewport_h = self.scroll_box.viewport_h;
+
+ if item_y < self.scroll_box.scroll_y {
+ self.scroll_box.scroll_y = item_y;
+ } else if item_y + self.item_height > self.scroll_box.scroll_y + viewport_h {
+ self.scroll_box.scroll_y = item_y + self.item_height - viewport_h;
+ }
+
+ self.mark_dirty(ctx);
+ return true;
+ }
+ }
+ false
}
- fn parent(&self, ctx: &UiContext) -> Option<*mut (dyn Element + 'static)> { self.scroll_box.parent(ctx) }
+ fn parent(&self, _ctx: &UiContext) -> Option<*mut (dyn Element + 'static)> { self.scroll_box.parent(_ctx) }
fn set_parent(&mut self, parent: Option<*mut (dyn Element + 'static)>, ctx: &mut UiContext) { self.scroll_box.set_parent(parent, ctx); }
fn children(&self, ctx: &UiContext) -> Vec<*mut (dyn Element + 'static)> { self.scroll_box.children(ctx) }
fn add_child(&mut self, child: *mut (dyn Element + 'static), ctx: &mut UiContext) { self.scroll_box.add_child(child, ctx); }
fn clear_children(&mut self, ctx: &mut UiContext) { self.scroll_box.clear_children(ctx); }
-
- fn as_scroll_controller(&self) -> Option<&dyn ScrollController> { Some(self) }
- fn as_scroll_controller_mut(&mut self) -> Option<&mut dyn ScrollController> { Some(self) }
-}
-
-impl ScrollController for List {
- fn update_bounds(&mut self, count: usize, viewport_y: f32, viewport_h: f32) {
- self.update_bounds(count, viewport_y, viewport_h);
- }
-
- fn get_item_draw_y(&self, idx: usize, offset: f32) -> Option<f32> {
- self.get_item_draw_y(idx, offset)
- }
}
unsafe impl Send for List {}
diff --git a/src/widget/mod.rs b/src/widget/mod.rs
index f102f18..4774277 100644
--- a/src/widget/mod.rs
+++ b/src/widget/mod.rs
@@ -543,12 +543,6 @@ pub trait Element {
let view_max = list.scroll_box.viewport_y + list.scroll_box.viewport_h - 4.0;
scroll_box_bounds = Some([sb_x, view_min, sb_x + sb_w, view_max]);
break;
- } else if let Some(col_list) = parent.as_any().downcast_ref::<crate::widget::ColumnarList>() {
- let (sb_x, _, sb_w, _) = col_list.rect();
- let view_min = col_list.scroll_box.viewport_y + 4.0;
- let view_max = col_list.scroll_box.viewport_y + col_list.scroll_box.viewport_h - 4.0;
- scroll_box_bounds = Some([sb_x, view_min, sb_x + sb_w, view_max]);
- break;
}
curr = parent.parent(ctx);
}
@@ -770,7 +764,7 @@ pub use self::input::{
pub use self::container::{
Container, ContainerLayout, OverlayLayout, ManualLayout, VerticalLayout, GridLayout, AdaptiveGridLayout,
ColumnsLayout, MosaicLayout, ReverseMosaicLayout,
- SectionContainer, Header, ContentBg, ParametersBg, List, ColumnarList, ListColumn, ListRow, ColumnWidth,
+ SectionContainer, Header, ContentBg, ParametersBg, List, ListColumn, ListRow, ColumnWidth,
ScrollBox, Menu, MenuBar, Spreadsheet, Breadcrumb, Plate,
Switcher, Layer, Page, Backplate, Paginator, ScrollBar, TreeList, TreeElement, ControlPanel
};