Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions _view_tree_test.v
Original file line number Diff line number Diff line change
Expand Up @@ -519,3 +519,43 @@ fn tree_child_ids(v View) []string {
}
return ids
}

fn test_tree_reveal_expands_once_per_selection() {
mut w := Window{}
cfg := TreeCfg{
id: 'reveal_tree'
selected: 'root/child'
reveal: ['root']
nodes: [
tree_node(
id: 'root'
text: 'Root'
nodes: [
tree_node(id: 'root/child', text: 'Child'),
]
),
]
}
w.tree(cfg)
assert w.view_state.tree_state.get('reveal_tree') or {
map[string]bool{}
}['root'] == true

// A collapse by the user survives, because `selected` did not move.
w.view_state.tree_state.set('reveal_tree', {
'root': false
})
w.tree(cfg)
assert w.view_state.tree_state.get('reveal_tree') or {
map[string]bool{}
}['root'] == false

// Moving `selected` reveals again.
w.tree(TreeCfg{
...cfg
selected: 'root'
})
assert w.view_state.tree_state.get('reveal_tree') or {
map[string]bool{}
}['root'] == true
}
2 changes: 2 additions & 0 deletions state_registry.v
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,8 @@ const ns_list_box_source = 'gui.list_box.source'
const ns_tree_focus = 'gui.tree.focus'
const ns_tree_lazy = 'gui.tree.lazy'
const cap_tree_lazy = 30
const ns_tree_reveal = 'gui.tree.reveal'
const cap_tree_reveal = 30
const ns_date_picker = 'gui.date_picker'
const ns_table_col_widths = 'gui.table.col_widths'
const ns_table_warned_no_id = 'gui.table.warned_no_id'
Expand Down
42 changes: 36 additions & 6 deletions view_tree.v
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ pub:
on_select fn (string, mut Window) = unsafe { nil }
on_lazy_load fn (string, string, mut Window) = unsafe { nil }
nodes []TreeNodeCfg
selected string // id of the highlighted node
reveal []string // ids to expand when `selected` changes
indent f32 = gui_theme.tree_style.indent
spacing f32 = gui_theme.tree_style.spacing
id_focus u32
Expand Down Expand Up @@ -87,10 +89,8 @@ struct TreeDragContext {
// tree creates a tree view from the given [TreeCfg](#TreeCfg).
// Uses flat-row rendering with optional spacer-based virtualization.
pub fn (mut window Window) tree(cfg TreeCfg) View {
tree_map := window.view_state.tree_state.get(cfg.id) or {
map[string]bool{}
}
cfg_id := cfg.id
tree_map := tree_apply_reveal(cfg, mut window)
mut lazy_sm := state_map[string, bool](mut window, ns_tree_lazy, cap_tree_lazy)

mut flat_rows := []TreeFlatRow{cap: cfg.nodes.len * 4}
Expand Down Expand Up @@ -118,6 +118,7 @@ pub fn (mut window Window) tree(cfg TreeCfg) View {
}

indent := cfg.indent
selected := cfg.selected
on_select := cfg.on_select
on_lazy_load := cfg.on_lazy_load
text_style_icon := tree_icon_style(cfg.nodes)
Expand Down Expand Up @@ -194,7 +195,7 @@ pub fn (mut window Window) tree(cfg TreeCfg) View {
sibling_index: sibling_idx
sibling_ids: sibling_ids_by_parent[pid]
}
content << tree_flat_row_view(cfg_id, on_select, on_lazy_load, fr, indent,
content << tree_flat_row_view(cfg_id, selected, on_select, on_lazy_load, fr, indent,
min_width_icon, is_draggable, drag_ctx)
}
}
Expand Down Expand Up @@ -374,6 +375,28 @@ fn tree_flat_row_content(flat_row TreeFlatRow, _indent f32, min_width_icon f32)
)
}

// tree_apply_reveal expands the ids in `cfg.reveal` when `cfg.selected`
// changes. A node the user collapses later stays collapsed, because the
// reveal runs again only after `selected` moves to another node.
fn tree_apply_reveal(cfg TreeCfg, mut window Window) map[string]bool {
mut tree_map := window.view_state.tree_state.get(cfg.id) or {
map[string]bool{}
}
if cfg.reveal.len == 0 {
return tree_map
Comment on lines +385 to +386

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Track selection changes even when reveal is empty

When the newly selected node has no ancestors, callers naturally pass an empty reveal, but this return leaves the stored selection unchanged. For example, selecting root/child with ['root'], then selecting root with [], collapsing root, and selecting root/child again causes the stored value to still equal root/child, so the final reveal is skipped and the selected child remains hidden. Update the stored selection before returning so every selection transition is observed.

Useful? React with 👍 / 👎.

}
mut revealed := state_map[string, string](mut window, ns_tree_reveal, cap_tree_reveal)
if revealed.get(cfg.id) or { '' } == cfg.selected {
return tree_map
}
revealed.set(cfg.id, cfg.selected)
for id in cfg.reveal {
tree_map[id] = true
Comment on lines +393 to +394

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Start lazy loading for nodes expanded by reveal

When a revealed ID identifies an unloaded lazy node, setting only tree_map[id] marks it expanded without setting its loading state or invoking on_lazy_load. tree_collect_flat_rows consequently renders neither children nor a loading row, leaving the externally selected descendant inaccessible until the user collapses and manually expands the node. Programmatic expansion should trigger the same guarded lazy-load path as click or right-arrow expansion.

Useful? React with 👍 / 👎.

}
window.view_state.tree_state.set(cfg.id, tree_map)
return tree_map
}

fn tree_arrow_icon(fr TreeFlatRow) string {
return match true {
!fr.has_children {
Expand All @@ -393,7 +416,7 @@ fn tree_arrow_icon(fr TreeFlatRow) string {
}

// tree_flat_row_view produces a single View for one flat row.
fn tree_flat_row_view(cfg_id string, on_select fn (string, mut Window), on_lazy_load fn (string, string, mut Window), flat_row TreeFlatRow, indent f32, min_width_icon f32, reorderable bool, drag_ctx TreeDragContext) View {
fn tree_flat_row_view(cfg_id string, selected string, on_select fn (string, mut Window), on_lazy_load fn (string, string, mut Window), flat_row TreeFlatRow, indent f32, min_width_icon f32, reorderable bool, drag_ctx TreeDragContext) View {
if flat_row.is_loading {
return row(
name: 'tree loading'
Expand Down Expand Up @@ -440,13 +463,17 @@ fn tree_flat_row_view(cfg_id string, on_select fn (string, mut Window), on_lazy_
}
}

is_selected := selected.len > 0 && selected == id

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include selection in the tree item's accessibility state

When selected matches a row, is_selected affects only its visual styling and hover behavior; the row's a11y_state remains solely expanded or none. Screen-reader users therefore receive no indication of which tree item is selected. Combine AccessState.selected with the existing expanded bit for matching rows.

Useful? React with 👍 / 👎.


return row(
name: 'tree node content'
id: if reorderable { 'tr_${cfg_id}_${id}' } else { '' }
a11y_role: .tree_item
a11y_label: node_text
a11y_state: node_a11y_state
spacing: 0
color: if is_selected { gui_theme.color_select } else { color_transparent }
radius: gui_theme.radius_small
padding: Padding{
left: f32(depth) * indent
}
Expand All @@ -471,8 +498,11 @@ fn tree_flat_row_view(cfg_id string, on_select fn (string, mut Window), on_lazy_
),
]
on_click: on_click_fn
on_hover: fn (mut layout Layout, mut e Event, mut w Window) {
on_hover: fn [is_selected] (mut layout Layout, mut e Event, mut w Window) {
w.set_mouse_cursor_pointing_hand()
if is_selected {
return
}
for mut child in layout.children {
child.shape.color = gui_theme.color_hover
}
Expand Down