T13b Trees: Tree, TreeNode, TreeSelect, Outline, Disclosure (review pending: codex 401)

This commit is contained in:
ZacharyZhang-NY committed 2026-09-25 19:59:11 -04:00
1 parent 2d6404f688
commit db3b993cac
22 files changed
+1969 -94

No files matched your search

+33 -29
View File
@@ -12,17 +12,44 @@ use crate::{
theme::{ActiveTheme, ControlSize, IconSize, Radius},
};
type OnCommit = Rc<dyn Fn(&SharedString, &mut Window, &mut App)>;
pub(crate) type OnCommit = Rc<dyn Fn(&SharedString, &mut Window, &mut App)>;
/// A text being edited in place: its field while open, and who hears the kept text.
#[derive(Default)]
struct Editing {
pub(crate) struct Editing {
field: Option<(Entity<TextInput>, Subscription)>,
on_commit: Option<OnCommit>,
pub on_commit: Option<OnCommit>,
}
impl Editing {
/// Opens a field on `value`, all of it selected and focused; Enter or leaving keeps it.
pub(crate) fn begin(state: &Entity<Editing>, value: String, window: &mut Window, cx: &mut App) {
let field = cx.new(|cx| {
let mut input = TextInput::new(window, cx);
input.set_text(value, cx);
input
});
let all = field.read(cx).text().len();
field.update(cx, |input, cx| input.select(0..all, cx));
window.focus(&field.read(cx).focus().clone());
let owner = state.clone();
let events = window.subscribe(&field, cx, move |_, event, window, cx| {
if matches!(event, InputEvent::Submit | InputEvent::Blur) {
owner.update(cx, |editing, cx| editing.finish(false, window, cx));
}
});
state.update(cx, |editing, cx| {
editing.field = Some((field, events));
cx.notify();
});
}
pub(crate) fn field(&self) -> Option<Entity<TextInput>> {
self.field.as_ref().map(|(field, _)| field.clone())
}
/// Ends editing; keeps the text unless `cancel`.
fn finish(&mut self, cancel: bool, window: &mut Window, cx: &mut Context<Self>) {
pub(crate) fn finish(&mut self, cancel: bool, window: &mut Window, cx: &mut Context<Self>) {
let Some((field, _)) = self.field.take() else {
return;
};
@@ -76,12 +103,7 @@ impl RenderOnce for InlineEdit {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let state = window.use_keyed_state(self.id.clone(), cx, |_, _| Editing::default());
state.update(cx, |editing, _| editing.on_commit = self.on_commit.clone());
if let Some(field) = state
.read(cx)
.field
.as_ref()
.map(|(field, _)| field.clone())
{
if let Some(field) = state.read(cx).field() {
let escape = state.clone();
return div()
.id(self.id)
@@ -117,25 +139,7 @@ impl RenderOnce for InlineEdit {
.on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default())
.on_click(move |_, window, cx| {
log::info!("inline edit: editing");
let value = value.to_string();
let field = cx.new(|cx| {
let mut input = TextInput::new(window, cx);
input.set_text(value, cx);
input
});
let all = field.read(cx).text().len();
field.update(cx, |input, cx| input.select(0..all, cx));
window.focus(&field.read(cx).focus().clone());
let owner = start.clone();
let events = window.subscribe(&field, cx, move |_, event, window, cx| {
if matches!(event, InputEvent::Submit | InputEvent::Blur) {
owner.update(cx, |editing, cx| editing.finish(false, window, cx));
}
});
start.update(cx, |editing, cx| {
editing.field = Some((field, events));
cx.notify();
});
Editing::begin(&start, value.to_string(), window, cx);
})
.child(if empty { self.placeholder } else { self.value })
.child(
+3 -1
View File
@@ -48,6 +48,7 @@ pub use avatar::AvatarUpload;
pub use card::{CheckboxCard, RadioCard};
pub use cascade::{Cascade, Cascader};
pub use catalogs::FontPicker;
pub(crate) use check::check_mark;
pub use check::{CheckState, Checkbox, CheckboxGroup};
pub use checked::{EmailInput, UrlInput, is_email, is_url};
pub use chips::ChoiceChips;
@@ -64,6 +65,7 @@ pub use files::{DropZone, FileInput};
pub use glyphs::{EmojiPicker, IconPicker};
pub use group::{InputAddon, InputGroup};
pub use hotkey::HotkeyInput;
pub(crate) use inline::Editing;
pub use inline::InlineEdit;
pub use input::{Input, PasswordInput};
pub use knob::Knob;
@@ -82,7 +84,7 @@ pub use radio::{Radio, RadioGroup};
pub use rating::Rating;
pub use search::SearchInput;
pub use select::Select;
pub(crate) use select::{Listing, listing};
pub(crate) use select::{Listing, field_button, field_text, listing};
pub use signature::{SignaturePad, Stroke};
pub use slider::{RangeSlider, Slider};
pub use stepper::Stepper;
+15
View File
@@ -20,6 +20,7 @@ pub struct Choice {
pub icon: Option<IconName>,
pub note: Option<SharedString>,
pub disabled: bool,
pub depth: usize,
}
impl Choice {
@@ -30,9 +31,16 @@ impl Choice {
icon: None,
note: None,
disabled: false,
depth: 0,
}
}
/// How far it sits under a parent, for choices that come from a tree.
pub fn depth(mut self, depth: usize) -> Self {
self.depth = depth;
self
}
pub fn icon(mut self, icon: IconName) -> Self {
self.icon = Some(icon);
self
@@ -108,6 +116,13 @@ pub(crate) fn option_row(
}),
)
})
.when(choice.depth > 0, |row| {
row.child(
div()
.flex_none()
.w(theme.tree_indent() * choice.depth as f32),
)
})
.when_some(choice.icon, |row, icon| {
row.child(Icon::new(icon).size(IconSize::Sm).color(colors.fg_muted))
})