use super::{
BridgeRetainedState, BridgeView, events, render_canvas, render_checkbox, render_data_table,
render_div, render_icon, render_image, render_list, render_popover, render_radio,
render_scroll, render_select, render_spacer, render_text, render_text_input, render_tree,
render_uniform_list,
};
use crate::bridge_text_input::BridgeTextInput;
use crate::ir::IrNode;
use gpui::{
AnyElement, Context, Entity, FocusHandle, ListAlignment, ListState, ScrollAnchor, ScrollHandle,
Window, px,
};
use std::collections::HashSet;
#[derive(Default)]
pub(crate) struct RenderPassState {
pub live_scroll_ids: HashSet<String>,
pub live_scroll_anchor_ids: HashSet<String>,
pub live_list_ids: HashSet<String>,
pub live_focus_ids: HashSet<String>,
pub live_text_input_ids: HashSet<String>,
pub registered_focus_callbacks: HashSet<String>,
}
pub(crate) struct RenderPass<'a> {
view_id: u64,
retained: &'a mut BridgeRetainedState,
state: RenderPassState,
}
impl<'a> RenderPass<'a> {
pub fn new(view_id: u64, retained: &'a mut BridgeRetainedState) -> Self {
Self {
view_id,
retained,
state: RenderPassState::default(),
}
}
pub fn finish(self) -> RenderPassState {
self.state
}
pub fn view_id(&self) -> u64 {
self.view_id
}
pub fn focus_visible(&self) -> bool {
self.retained.focus_visible
}
pub fn render_node(
&mut self,
path: &str,
ir: &IrNode,
parent_scroll_handle: Option<ScrollHandle>,
window: &mut Window,
cx: &mut Context<BridgeView>,
) -> AnyElement {
match ir {
IrNode::Text { .. } | IrNode::TextInput { .. } | IrNode::Textarea { .. } => {
self.render_text_node(path, ir, window, cx)
}
IrNode::Scroll { .. }
| IrNode::Popover { .. }
| IrNode::UniformList { .. }
| IrNode::List { .. }
| IrNode::DataTable(_)
| IrNode::Tree(_)
| IrNode::Button(_)
| IrNode::Div(_) => {
self.render_container_node(path, ir, parent_scroll_handle, window, cx)
}
IrNode::Select(_) | IrNode::Checkbox(_) | IrNode::Radio(_) => {
self.render_control_node(path, ir, window, cx)
}
IrNode::Canvas(_)
| IrNode::Image { .. }
| IrNode::Icon { .. }
| IrNode::Spacer { .. } => self.render_leaf_node(path, ir, window, cx),
}
}
fn render_text_node(
&mut self,
path: &str,
ir: &IrNode,
window: &mut Window,
cx: &mut Context<BridgeView>,
) -> AnyElement {
match ir {
IrNode::Text {
id,
content,
runs,
style,
click,
} => render_text::render(
self,
path,
id.as_deref(),
content,
runs,
style,
click.as_deref(),
),
IrNode::TextInput {
id,
value,
placeholder,
style,
disabled,
tab_index,
shortcuts,
change,
focus,
blur,
context_menu,
} => self.render_text_entry(
path,
id.as_deref(),
value,
placeholder,
style,
*disabled,
*tab_index,
shortcuts,
change.as_deref(),
focus.as_deref(),
blur.as_deref(),
context_menu.as_deref(),
false,
window,
cx,
),
IrNode::Textarea {
id,
value,
placeholder,
style,
disabled,
tab_index,
shortcuts,
change,
focus,
blur,
context_menu,
} => self.render_text_entry(
path,
id.as_deref(),
value,
placeholder,
style,
*disabled,
*tab_index,
shortcuts,
change.as_deref(),
focus.as_deref(),
blur.as_deref(),
context_menu.as_deref(),
true,
window,
cx,
),
_ => unreachable!("render_text_node called with non-text IR node"),
}
}
#[allow(clippy::too_many_arguments)]
fn render_text_entry(
&mut self,
path: &str,
id: Option<&str>,
value: &str,
placeholder: &str,
style: &crate::ir::DivStyle,
disabled: bool,
tab_index: Option<isize>,
shortcuts: &std::sync::Arc<[crate::ir::ShortcutBinding]>,
change: Option<&str>,
focus: Option<&str>,
blur: Option<&str>,
context_menu: Option<&str>,
multiline: bool,
window: &mut Window,
cx: &mut Context<BridgeView>,
) -> AnyElement {
render_text_input::render(
self,
render_text_input::TextInputSpec {
path,
id,
value,
placeholder,
style,
disabled,
tab_index,
shortcuts,
change,
focus,
blur,
context_menu,
multiline,
},
window,
cx,
)
}
fn render_container_node(
&mut self,
path: &str,
ir: &IrNode,
parent_scroll_handle: Option<ScrollHandle>,
window: &mut Window,
cx: &mut Context<BridgeView>,
) -> AnyElement {
match ir {
IrNode::Scroll {
id,
axis,
style,
children,
} => render_scroll::render(
self,
render_scroll::ScrollSpec {
path,
id: id.as_deref(),
axis: *axis,
style,
children,
},
window,
cx,
),
IrNode::Popover {
id,
label,
open,
style,
popover_style,
anchor,
anchor_position,
anchor_offset,
anchor_position_mode,
anchor_fit,
snap_margin,
close_on_click_outside,
stack_priority,
disabled,
click,
close,
children,
} => render_popover::render(
self,
render_popover::PopoverSpec {
path,
id: id.as_deref(),
label,
open: *open,
style,
popover_style,
anchor: *anchor,
anchor_position: *anchor_position,
anchor_offset: *anchor_offset,
anchor_position_mode: *anchor_position_mode,
anchor_fit: *anchor_fit,
snap_margin: *snap_margin,
close_on_click_outside: *close_on_click_outside,
stack_priority: stack_priority.unwrap_or(1),
disabled: *disabled,
click: click.as_deref(),
close: close.as_deref(),
children,
},
window,
cx,
),
IrNode::UniformList {
id,
items,
style,
item_style,
click,
context_menu,
} => render_uniform_list::render(
self,
path,
id.as_deref(),
items,
style,
item_style,
click.as_deref(),
context_menu.as_deref(),
cx,
),
IrNode::List {
id,
items,
style,
item_style,
click,
context_menu,
} => render_list::render(
self,
path,
id.as_deref(),
items,
style,
item_style,
click.as_deref(),
context_menu.as_deref(),
window,
cx,
),
IrNode::DataTable(node) => render_data_table::render(self, path, node, window, cx),
IrNode::Tree(node) => render_tree::render(self, path, node, window, cx),
IrNode::Button(div) | IrNode::Div(div) => {
render_div::render(self, path, div, parent_scroll_handle, window, cx)
}
_ => unreachable!("render_container_node called with non-container IR node"),
}
}
fn render_control_node(
&mut self,
path: &str,
ir: &IrNode,
window: &mut Window,
cx: &mut Context<BridgeView>,
) -> AnyElement {
match ir {
IrNode::Select(node) => render_select::render(self, path, node, window, cx),
IrNode::Checkbox(node) => render_checkbox::render(self, path, node, window, cx),
IrNode::Radio(node) => render_radio::render(self, path, node, window, cx),
_ => unreachable!("render_control_node called with non-control IR node"),
}
}
fn render_leaf_node(
&mut self,
path: &str,
ir: &IrNode,
window: &mut Window,
cx: &mut Context<BridgeView>,
) -> AnyElement {
match ir {
IrNode::Canvas(node) => render_canvas::render(self.view_id, path, node, window, cx),
IrNode::Image {
id,
source,
style,
object_fit,
grayscale,
} => render_image::render(
self,
path,
id.as_deref(),
source,
style,
*object_fit,
*grayscale,
),
IrNode::Icon { id, source, style } => {
render_icon::render(self, path, id.as_deref(), source, style)
}
IrNode::Spacer { id, style } => render_spacer::render(self, path, id.as_deref(), style),
_ => unreachable!("render_leaf_node called with non-leaf IR node"),
}
}
pub fn render_children(
&mut self,
path: &str,
children: &[IrNode],
parent_scroll_handle: Option<ScrollHandle>,
window: &mut Window,
cx: &mut Context<BridgeView>,
) -> Vec<AnyElement> {
children
.iter()
.enumerate()
.map(|(index, child)| {
self.render_node(
&format!("{path}.{index}"),
child,
parent_scroll_handle.clone(),
window,
cx,
)
})
.collect()
}
pub fn retain_scroll_handle(&mut self, node_id: &str) -> ScrollHandle {
self.state.live_scroll_ids.insert(node_id.to_owned());
self.retained
.scroll_handles
.entry(node_id.to_owned())
.or_default()
.clone()
}
pub fn retain_scroll_anchor(
&mut self,
node_id: &str,
scroll_handle: &ScrollHandle,
request_scroll: bool,
) -> (ScrollAnchor, bool) {
self.state.live_scroll_anchor_ids.insert(node_id.to_owned());
let anchor = self
.retained
.scroll_anchors
.entry(node_id.to_owned())
.or_insert_with(|| ScrollAnchor::for_handle(scroll_handle.clone()))
.clone();
let should_scroll = if request_scroll {
self.retained
.requested_scroll_anchor_ids
.insert(node_id.to_owned())
} else {
self.retained.requested_scroll_anchor_ids.remove(node_id);
false
};
(anchor, should_scroll)
}
pub fn retain_list_state(&mut self, node_id: &str, item_count: usize) -> ListState {
self.state.live_list_ids.insert(node_id.to_owned());
let state = self
.retained
.list_states
.entry(node_id.to_owned())
.or_insert_with(|| ListState::new(item_count, ListAlignment::Top, px(500.0)));
if state.item_count() != item_count {
state.reset(item_count);
}
state.clone()
}
pub fn ensure_focus_handle(
&mut self,
node_id: &str,
cx: &mut Context<BridgeView>,
tab_stop: Option<bool>,
tab_index: Option<isize>,
) -> FocusHandle {
self.state.live_focus_ids.insert(node_id.to_owned());
let handle = self
.retained
.focus_handles
.entry(node_id.to_owned())
.or_insert_with(|| cx.focus_handle())
.clone();
let handle = match tab_stop {
Some(tab_stop) => handle.tab_stop(tab_stop),
None => handle,
};
match tab_index {
Some(tab_index) => handle.tab_index(tab_index),
None => handle,
}
}
pub fn register_focus_callbacks(
&mut self,
node_id: &str,
focus_handle: &FocusHandle,
focus: Option<&str>,
blur: Option<&str>,
window: &mut Window,
cx: &mut Context<BridgeView>,
) {
let Some(_) = focus.or(blur) else {
return;
};
if self.state.registered_focus_callbacks.contains(node_id) {
return;
}
let view_id = self.view_id;
if let Some(callback_id) = focus {
let focus_node_id = node_id.to_owned();
let callback_id = callback_id.to_owned();
let subscription = cx.on_focus(focus_handle, window, move |_, _, _| {
events::emit_focus(view_id, &focus_node_id, &callback_id);
});
self.retained.focus_subscriptions.push(subscription);
}
if let Some(callback_id) = blur {
let blur_node_id = node_id.to_owned();
let callback_id = callback_id.to_owned();
let subscription = cx.on_blur(focus_handle, window, move |_, _, _| {
events::emit_blur(view_id, &blur_node_id, &callback_id);
});
self.retained.focus_subscriptions.push(subscription);
}
self.state
.registered_focus_callbacks
.insert(node_id.to_owned());
}
pub fn mark_text_input_live(&mut self, node_id: &str) {
self.state.live_text_input_ids.insert(node_id.to_owned());
}
pub fn text_input_entity(&mut self, node_id: &str) -> Option<Entity<BridgeTextInput>> {
self.retained.text_inputs.get(node_id).cloned()
}
pub fn insert_text_input_entity(&mut self, node_id: &str, entity: Entity<BridgeTextInput>) {
self.retained.text_inputs.insert(node_id.to_owned(), entity);
}
}
#[cfg(test)]
mod tests {
use super::RenderPass;
use crate::{bridge_view::BridgeView, ir::IrNode};
#[gpui::test]
fn register_focus_callbacks_dedupes_per_node(cx: &mut gpui::TestAppContext) {
let (view, cx) = cx.add_window_view(|_, _| BridgeView {
view_id: 7,
ir: IrNode::text("hello"),
retained: Default::default(),
});
view.update_in(cx, |view, window, view_cx| {
let focus_handle = view_cx.focus_handle();
let mut pass = RenderPass::new(view.view_id, &mut view.retained);
pass.register_focus_callbacks(
"field",
&focus_handle,
Some("focused"),
Some("blurred"),
window,
view_cx,
);
pass.register_focus_callbacks(
"field",
&focus_handle,
Some("focused"),
Some("blurred"),
window,
view_cx,
);
let state = pass.finish();
assert_eq!(view.retained.focus_subscriptions.len(), 2);
assert!(state.registered_focus_callbacks.contains("field"));
});
}
}