Skip to main content

native/guppy_nif/src/bridge_view/render_div.rs

use super::{
    events::{self, BridgeDragState},
    identity::NodeIdentity,
    render_pass::RenderPass,
    style::{apply_div_style, apply_refinement_style},
};
use crate::bridge_view::BridgeView;
use crate::ir::{AnimationSpec, DivNode, ShortcutBinding, empty_shortcuts};
use gpui::{
    Animation, AnimationExt, AnyElement, AppContext, Context, Div, Empty, FocusHandle,
    InteractiveElement, IntoElement, KeyDownEvent, KeyUpEvent, MouseButton, MouseDownEvent,
    MouseMoveEvent, MouseUpEvent, ParentElement, Render, ScrollAnchor, ScrollHandle, SharedString,
    Stateful, StatefulInteractiveElement, Styled, Window, deferred, div, px, rgb,
};
use std::{sync::Arc, time::Duration};

struct DisabledEventFilter {
    disabled: bool,
}

impl DisabledEventFilter {
    fn new(disabled: bool) -> Self {
        Self { disabled }
    }

    fn callback<'a>(&self, callback: Option<&'a str>) -> Option<&'a str> {
        if self.disabled { None } else { callback }
    }

    fn shortcuts(&self, shortcuts: &Arc<[ShortcutBinding]>) -> Arc<[ShortcutBinding]> {
        if self.disabled {
            empty_shortcuts()
        } else {
            shortcuts.clone()
        }
    }

    fn tab_stop(&self, tab_stop: Option<bool>) -> Option<bool> {
        if self.disabled { None } else { tab_stop }
    }

    fn tab_index(&self, tab_index: Option<isize>) -> Option<isize> {
        if self.disabled { None } else { tab_index }
    }

    fn focusable(&self, focusable: bool) -> bool {
        focusable && !self.disabled
    }
}

struct DivPrepared<'a> {
    identity: DivIdentity,
    interactions: DivInteractionSpec<'a>,
    focus: DivFocusSpec,
    scroll: DivScrollSpec,
}

impl DivPrepared<'_> {
    fn wants_focusable_element(&self) -> bool {
        self.interactions.keyboard_actionable || self.focus.focusable
    }
}

struct DivIdentity {
    view_id: u64,
    node_id: NodeIdentity,
    node_key: String,
}

struct DivInteractionSpec<'a> {
    click: Option<&'a str>,
    hover: Option<&'a str>,
    focus: Option<&'a str>,
    blur: Option<&'a str>,
    key_down: Option<&'a str>,
    key_up: Option<&'a str>,
    context_menu: Option<&'a str>,
    drag_start: Option<&'a str>,
    drag_move: Option<&'a str>,
    drop: Option<&'a str>,
    mouse_down: Option<&'a str>,
    mouse_up: Option<&'a str>,
    mouse_move: Option<&'a str>,
    scroll_wheel: Option<&'a str>,
    shortcuts: Arc<[ShortcutBinding]>,
    keyboard_actionable: bool,
}

struct DivFocusSpec {
    focusable: bool,
    tab_stop: Option<bool>,
    tab_index: Option<isize>,
    needs_focus_handle: bool,
}

struct DivScrollSpec {
    anchor_scroll: bool,
    scroll_to: bool,
}

struct DivRetainedState {
    tracked_scroll_handle: Option<ScrollHandle>,
    focus_handle: Option<FocusHandle>,
}

pub(crate) fn render(
    pass: &mut RenderPass<'_>,
    path: &str,
    node: &DivNode,
    parent_scroll_handle: Option<ScrollHandle>,
    window: &mut Window,
    cx: &mut Context<BridgeView>,
) -> AnyElement {
    let prepared = prepare_div(pass.view_id(), path, node);
    let retained = prepare_div_retained_state(pass, node, &prepared, window, cx);
    let child_elements = render_div_children(
        pass,
        path,
        node,
        parent_scroll_handle.clone(),
        &retained,
        window,
        cx,
    );

    let styled_div = build_base_div(&prepared.identity, node, child_elements);
    let styled_div = attach_scroll_and_focus(
        styled_div,
        &prepared,
        &retained,
        parent_scroll_handle.as_ref(),
        pass,
        window,
        cx,
    );
    let styled_div = attach_pointer_and_keyboard_interactions(styled_div, &prepared, &retained);
    let styled_div = attach_tooltip(styled_div, node);
    let styled_div = apply_stateful_style_refinements(styled_div, pass, node, &retained, window);

    finalize_div_layering(styled_div, node)
}

fn prepare_div<'a>(view_id: u64, path: &str, node: &'a DivNode) -> DivPrepared<'a> {
    let identity = prepare_div_identity(view_id, path, node.id.as_deref());
    let disabled = DisabledEventFilter::new(node.disabled);

    let click = disabled.callback(node.click.as_deref());
    let context_menu = disabled.callback(node.context_menu.as_deref());
    let shortcuts = disabled.shortcuts(&node.shortcuts);
    let keyboard_actionable = click.is_some() || context_menu.is_some() || !shortcuts.is_empty();

    let focusable = disabled.focusable(node.focusable);
    let tab_stop = disabled.tab_stop(node.tab_stop);
    let tab_stop = if keyboard_actionable {
        Some(tab_stop.unwrap_or(true))
    } else {
        tab_stop
    };
    let tab_index = disabled.tab_index(node.tab_index);

    let interactions = DivInteractionSpec {
        click,
        hover: disabled.callback(node.hover.as_deref()),
        focus: disabled.callback(node.focus.as_deref()),
        blur: disabled.callback(node.blur.as_deref()),
        key_down: disabled.callback(node.key_down.as_deref()),
        key_up: disabled.callback(node.key_up.as_deref()),
        context_menu,
        drag_start: disabled.callback(node.drag_start.as_deref()),
        drag_move: disabled.callback(node.drag_move.as_deref()),
        drop: disabled.callback(node.drop.as_deref()),
        mouse_down: disabled.callback(node.mouse_down.as_deref()),
        mouse_up: disabled.callback(node.mouse_up.as_deref()),
        mouse_move: disabled.callback(node.mouse_move.as_deref()),
        scroll_wheel: disabled.callback(node.scroll_wheel.as_deref()),
        shortcuts,
        keyboard_actionable,
    };

    let focus = DivFocusSpec {
        focusable,
        tab_stop,
        tab_index,
        needs_focus_handle: interactions.keyboard_actionable
            || focusable
            || tab_stop.is_some()
            || tab_index.is_some()
            || !node.focus_style.is_empty()
            || !node.focus_visible_style.is_empty()
            || !node.in_focus_style.is_empty()
            || interactions.focus.is_some()
            || interactions.blur.is_some()
            || interactions.key_down.is_some()
            || interactions.key_up.is_some(),
    };

    DivPrepared {
        identity,
        interactions,
        focus,
        scroll: DivScrollSpec {
            anchor_scroll: node.anchor_scroll,
            scroll_to: node.scroll_to,
        },
    }
}

fn prepare_div_identity(view_id: u64, path: &str, explicit_id: Option<&str>) -> DivIdentity {
    let node_id = NodeIdentity::new(view_id, path, explicit_id);
    let node_key = node_id.to_string();

    DivIdentity {
        view_id,
        node_id,
        node_key,
    }
}

fn prepare_div_retained_state(
    pass: &mut RenderPass<'_>,
    node: &DivNode,
    prepared: &DivPrepared<'_>,
    window: &mut Window,
    cx: &mut Context<BridgeView>,
) -> DivRetainedState {
    let tracked_scroll_handle = if node.track_scroll {
        Some(pass.retain_scroll_handle(&prepared.identity.node_key))
    } else {
        None
    };

    let focus_handle = if prepared.focus.needs_focus_handle {
        Some(pass.ensure_focus_handle(
            &prepared.identity.node_key,
            cx,
            prepared.focus.tab_stop,
            prepared.focus.tab_index,
        ))
    } else {
        None
    };

    if let Some(handle) = focus_handle.as_ref() {
        pass.register_focus_callbacks(
            &prepared.identity.node_key,
            handle,
            prepared.interactions.focus,
            prepared.interactions.blur,
            window,
            cx,
        );
    }

    DivRetainedState {
        tracked_scroll_handle,
        focus_handle,
    }
}

fn render_div_children(
    pass: &mut RenderPass<'_>,
    path: &str,
    node: &DivNode,
    parent_scroll_handle: Option<ScrollHandle>,
    retained: &DivRetainedState,
    window: &mut Window,
    cx: &mut Context<BridgeView>,
) -> Vec<AnyElement> {
    let child_scroll_handle = retained
        .tracked_scroll_handle
        .clone()
        .or(parent_scroll_handle);

    pass.render_children(path, &node.children, child_scroll_handle, window, cx)
}

fn build_base_div(
    identity: &DivIdentity,
    node: &DivNode,
    child_elements: Vec<AnyElement>,
) -> Stateful<Div> {
    let styled_div = apply_div_style(
        div()
            .id(identity.node_id.to_shared_string())
            .children(child_elements),
        &node.style,
    );

    if node.occlude {
        styled_div.occlude()
    } else {
        styled_div
    }
}

fn attach_scroll_and_focus(
    styled_div: Stateful<Div>,
    prepared: &DivPrepared<'_>,
    retained: &DivRetainedState,
    parent_scroll_handle: Option<&ScrollHandle>,
    pass: &mut RenderPass<'_>,
    window: &mut Window,
    cx: &mut Context<BridgeView>,
) -> Stateful<Div> {
    let styled_div = match retained.tracked_scroll_handle.as_ref() {
        Some(handle) => styled_div.track_scroll(handle),
        None => styled_div,
    };

    let styled_div = if prepared.scroll.anchor_scroll {
        match parent_scroll_handle {
            Some(handle) => {
                let (anchor, should_scroll) = pass.retain_scroll_anchor(
                    &prepared.identity.node_key,
                    handle,
                    prepared.scroll.scroll_to,
                );

                if should_scroll {
                    schedule_scroll_to_anchor(anchor.clone(), window, cx);
                }

                styled_div.anchor_scroll(Some(anchor))
            }
            None => styled_div,
        }
    } else {
        styled_div
    };

    match retained.focus_handle.as_ref() {
        Some(handle) => {
            let styled_div = styled_div.track_focus(handle);
            if prepared.wants_focusable_element() {
                styled_div.focusable()
            } else {
                styled_div
            }
        }
        None => styled_div,
    }
}

fn schedule_scroll_to_anchor(
    anchor: ScrollAnchor,
    window: &mut Window,
    cx: &mut Context<BridgeView>,
) {
    anchor.scroll_to(window, cx);
    cx.notify();
}

fn attach_pointer_and_keyboard_interactions(
    styled_div: Stateful<Div>,
    prepared: &DivPrepared<'_>,
    retained: &DivRetainedState,
) -> Stateful<Div> {
    let view_id = prepared.identity.view_id;
    let node_key = &prepared.identity.node_key;
    let interactions = &prepared.interactions;

    let styled_div = match interactions.hover {
        Some(callback_id) => {
            let callback_id = callback_id.to_owned();
            let hover_node_id = node_key.clone();
            styled_div.on_hover(move |hovered, _, _| {
                events::emit_hover(view_id, &hover_node_id, &callback_id, *hovered);
            })
        }
        None => styled_div,
    };

    let styled_div = match retained.focus_handle.as_ref() {
        Some(handle) => {
            let handle = handle.clone();
            styled_div.on_any_mouse_down(move |_, window, _| {
                handle.focus(window);
            })
        }
        None => styled_div,
    };

    let styled_div = if interactions.key_down.is_some()
        || interactions.context_menu.is_some()
        || !interactions.shortcuts.is_empty()
    {
        let key_down_callback_id = interactions.key_down.map(str::to_owned);
        let context_menu_callback_id = interactions.context_menu.map(str::to_owned);
        let key_down_node_id = node_key.clone();
        let shortcut_bindings = interactions.shortcuts.clone();

        styled_div.on_key_down(move |event: &KeyDownEvent, _, cx| {
            if let Some(callback_id) = key_down_callback_id.as_ref() {
                events::emit_key_down(view_id, &key_down_node_id, callback_id, event);
            }

            if let Some(callback_id) = context_menu_callback_id.as_ref()
                && events::is_context_menu_key(event)
            {
                events::emit_keyboard_context_menu(view_id, &key_down_node_id, callback_id, event);
                cx.stop_propagation();
                return;
            }

            if let Some(shortcut) = events::matching_shortcut(event, &shortcut_bindings) {
                events::emit_action(view_id, &key_down_node_id, shortcut, event);
                cx.stop_propagation();
            }
        })
    } else {
        styled_div
    };

    let styled_div = match interactions.key_up {
        Some(callback_id) => {
            let callback_id = callback_id.to_owned();
            let key_up_node_id = node_key.clone();
            styled_div.on_key_up(move |event: &KeyUpEvent, _, _| {
                events::emit_key_up(view_id, &key_up_node_id, &callback_id, event);
            })
        }
        None => styled_div,
    };

    let styled_div = match interactions.context_menu {
        Some(callback_id) => {
            let callback_id = callback_id.to_owned();
            let context_menu_node_id = node_key.clone();
            styled_div.on_mouse_down(MouseButton::Right, move |event: &MouseDownEvent, _, _| {
                events::emit_context_menu(view_id, &context_menu_node_id, &callback_id, event);
            })
        }
        None => styled_div,
    };

    let styled_div = match interactions.drop {
        Some(callback_id) => {
            let callback_id = callback_id.to_owned();
            let drop_node_id = node_key.clone();
            styled_div.on_drop::<BridgeDragState>(move |drag, _, _| {
                events::emit_drop(view_id, &drop_node_id, &callback_id, &drag.source_id);
            })
        }
        None => styled_div,
    };

    let styled_div = match interactions.drag_move {
        Some(callback_id) => {
            let callback_id = callback_id.to_owned();
            let drag_move_node_id = node_key.clone();
            styled_div.on_drag_move::<BridgeDragState>(move |event, _, cx| {
                let drag = event.drag(cx);
                events::emit_drag_move(
                    view_id,
                    &drag_move_node_id,
                    &callback_id,
                    &drag.source_id,
                    event.event.pressed_button,
                    event.event.position,
                    &event.event.modifiers,
                );
            })
        }
        None => styled_div,
    };

    let styled_div = match interactions.drag_start {
        Some(callback_id) => {
            let callback_id = callback_id.to_owned();
            let drag_start_node_id = node_key.clone();
            let drag_source_id = node_key.clone();
            styled_div.on_drag(
                BridgeDragState {
                    source_id: drag_source_id,
                },
                move |drag, _, _, cx| {
                    events::emit_drag_start(
                        view_id,
                        &drag_start_node_id,
                        &callback_id,
                        &drag.source_id,
                    );
                    cx.new(|_| Empty)
                },
            )
        }
        None if interactions.drag_move.is_some() => {
            let drag_source_id = node_key.clone();
            styled_div.on_drag(
                BridgeDragState {
                    source_id: drag_source_id,
                },
                move |_, _, _, cx| cx.new(|_| Empty),
            )
        }
        None => styled_div,
    };

    let styled_div = match interactions.mouse_down {
        Some(callback_id) => {
            let callback_id = callback_id.to_owned();
            let mouse_down_node_id = node_key.clone();
            styled_div.on_any_mouse_down(move |event: &MouseDownEvent, _, _| {
                events::emit_mouse_down(view_id, &mouse_down_node_id, &callback_id, event);
            })
        }
        None => styled_div,
    };

    let styled_div = match interactions.mouse_up {
        Some(callback_id) => {
            let callback_id = callback_id.to_owned();
            let mouse_up_node_id = node_key.clone();
            styled_div.capture_any_mouse_up(move |event: &MouseUpEvent, _, _| {
                events::emit_mouse_up(view_id, &mouse_up_node_id, &callback_id, event);
            })
        }
        None => styled_div,
    };

    let styled_div = match interactions.mouse_move {
        Some(callback_id) => {
            let callback_id = callback_id.to_owned();
            let mouse_move_node_id = node_key.clone();
            styled_div.on_mouse_move(move |event: &MouseMoveEvent, _, _| {
                events::emit_mouse_move(view_id, &mouse_move_node_id, &callback_id, event);
            })
        }
        None => styled_div,
    };

    let styled_div = match interactions.scroll_wheel {
        Some(callback_id) => {
            let callback_id = callback_id.to_owned();
            let scroll_node_id = node_key.clone();
            styled_div.on_scroll_wheel(move |event: &gpui::ScrollWheelEvent, _, _| {
                events::emit_scroll_wheel(view_id, &scroll_node_id, &callback_id, event);
            })
        }
        None => styled_div,
    };

    match interactions.click {
        Some(callback_id) => {
            let callback_id = callback_id.to_owned();
            let click_node_id = node_key.clone();
            styled_div.on_click(move |_, _, _| {
                events::emit_click(view_id, &click_node_id, &callback_id);
            })
        }
        None => styled_div,
    }
}

fn attach_tooltip(mut styled_div: Stateful<Div>, node: &DivNode) -> Stateful<Div> {
    if node.disabled {
        return styled_div;
    }

    if let Some(tooltip) = node.tooltip.as_ref() {
        let tooltip = tooltip.clone();
        styled_div = styled_div.tooltip(move |_, cx| {
            cx.new(|_| TooltipView {
                text: tooltip.clone(),
            })
            .into()
        });
    }

    styled_div
}

struct TooltipView {
    text: String,
}

impl Render for TooltipView {
    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
        div()
            .p(px(6.0))
            .rounded_md()
            .border_1()
            .bg(rgb(0x111827))
            .text_color(rgb(0xf9fafb))
            .child(self.text.clone())
    }
}

fn apply_stateful_style_refinements(
    mut styled_div: Stateful<Div>,
    pass: &RenderPass<'_>,
    node: &DivNode,
    retained: &DivRetainedState,
    window: &Window,
) -> Stateful<Div> {
    if !node.disabled
        && !node.focus_visible_style.is_empty()
        && pass.focus_visible()
        && retained
            .focus_handle
            .as_ref()
            .is_some_and(|handle| handle.is_focused(window))
    {
        styled_div = apply_div_style(styled_div, &node.focus_visible_style);
    }

    if !node.disabled && !node.focus_style.is_empty() {
        let focus_ops = node.focus_style.clone();
        styled_div = styled_div.focus(move |style| apply_refinement_style(style, &focus_ops));
    }

    if !node.disabled && !node.in_focus_style.is_empty() {
        let in_focus_ops = node.in_focus_style.clone();
        styled_div = styled_div.in_focus(move |style| apply_refinement_style(style, &in_focus_ops));
    }

    if !node.disabled && !node.hover_style.is_empty() {
        let hover_ops = node.hover_style.clone();
        styled_div = styled_div.hover(move |style| apply_refinement_style(style, &hover_ops));
    }

    if !node.disabled && !node.active_style.is_empty() {
        let active_ops = node.active_style.clone();
        styled_div = styled_div.active(move |style| apply_refinement_style(style, &active_ops));
    }

    if node.disabled && !node.disabled_style.is_empty() {
        styled_div = apply_div_style(styled_div, &node.disabled_style);
    }

    styled_div
}

fn finalize_div_layering(styled_div: Stateful<Div>, node: &DivNode) -> AnyElement {
    let element = match node.animation.as_ref() {
        Some(animation) => animate_div(styled_div, animation).into_any_element(),
        None => styled_div.into_any_element(),
    };

    match node.stack_priority {
        Some(priority) => deferred(element).with_priority(priority).into_any_element(),
        None => element,
    }
}

fn animate_div(styled_div: Stateful<Div>, spec: &AnimationSpec) -> impl IntoElement {
    let mut animation = Animation::new(Duration::from_millis(spec.duration_ms));
    if spec.repeat {
        animation = animation.repeat();
    }

    let from = spec.from;
    let to = spec.to;
    styled_div.with_animation(
        SharedString::from(spec.id.clone()),
        animation,
        move |element, delta| element.opacity(animated_opacity_value(from, to, delta)),
    )
}

fn animated_opacity_value(from: f32, to: f32, delta: f32) -> f32 {
    from + ((to - from) * delta)
}

#[cfg(test)]
mod tests {
    use super::{DisabledEventFilter, animated_opacity_value, prepare_div};
    use crate::ir::{DivNode, IrNode, ShortcutBinding};
    use std::sync::Arc;

    fn test_div_node() -> DivNode {
        DivNode {
            id: Some("test_div".into()),
            style: Arc::new([]),
            hover_style: Arc::new([]),
            focus_style: Arc::new([]),
            focus_visible_style: Arc::new([]),
            in_focus_style: Arc::new([]),
            active_style: Arc::new([]),
            disabled_style: Arc::new([]),
            animation: None,
            disabled: false,
            stack_priority: None,
            occlude: false,
            focusable: false,
            tab_stop: None,
            tab_index: None,
            track_scroll: false,
            anchor_scroll: false,
            scroll_to: false,
            tooltip: None,
            shortcuts: Arc::new([]),
            children: Vec::<IrNode>::new().into(),
            click: None,
            hover: None,
            focus: None,
            blur: None,
            key_down: None,
            key_up: None,
            context_menu: None,
            drag_start: None,
            drag_move: None,
            drop: None,
            mouse_down: None,
            mouse_up: None,
            mouse_move: None,
            scroll_wheel: None,
        }
    }

    #[test]
    fn context_menu_callbacks_are_keyboard_actionable() {
        let mut node = test_div_node();
        node.context_menu = Some("open_context_menu".into());

        let prepared = prepare_div(7, "root", &node);

        assert!(prepared.interactions.keyboard_actionable);
        assert!(prepared.focus.needs_focus_handle);
        assert_eq!(prepared.focus.tab_stop, Some(true));
    }

    #[test]
    fn disabled_shortcuts_reuse_empty_slice() {
        let filter = DisabledEventFilter::new(true);
        let shortcuts: Arc<[ShortcutBinding]> = Arc::new([]);

        let first = filter.shortcuts(&shortcuts);
        let second = filter.shortcuts(&shortcuts);

        assert!(Arc::ptr_eq(&first, &second));
    }

    #[test]
    fn animated_opacity_interpolates_between_bounds() {
        assert_eq!(animated_opacity_value(0.25, 1.0, 0.0), 0.25);
        assert_eq!(animated_opacity_value(0.25, 1.0, 1.0), 1.0);
        assert_eq!(animated_opacity_value(0.25, 1.0, 0.5), 0.625);
    }
}