Skip to main content

native/guppy_nif/src/bridge_view/render_pass.rs

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"));
        });
    }
}