258 lines · 8.3 KB · Rust
RawDownload
//! Trigger button + [`CodeReviewDiffMenu`] overlay for picking the diff//! target in the code review header.use pathfinder_geometry::vector::vec2f;use warp_core::ui::theme::Fill;use warpui::elements::{    ChildAnchor, ChildView, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, Element,    Flex, MouseStateHandle, OffsetPositioning, ParentAnchor, ParentElement, ParentOffsetBounds,    Radius, Stack, Text,};use warpui::fonts::{Properties, Weight};use warpui::keymap::FixedBinding;use warpui::platform::Cursor;use warpui::text_layout::ClipConfig;use warpui::ui_components::button::ButtonVariant;use warpui::ui_components::components::{Coords, UiComponent, UiComponentStyles};use warpui::{    AppContext, Entity, FocusContext, SingletonEntity as _, TypedActionView, View, ViewContext,    ViewHandle, id,}; use crate::appearance::Appearance;use crate::code_review::diff_menu::{CodeReviewDiffMenu, CodeReviewDiffMenuEvent};use crate::code_review::diff_state::DiffMode;use crate::ui_components::icons::Icon; /// A single selectable target in the diff selector menu.#[derive(Debug, Clone)]pub struct DiffTarget {    pub label: String,    pub mode: DiffMode,    pub is_selected: bool,} impl DiffTarget {    pub fn new(label: impl Into<String>, mode: DiffMode, is_selected: bool) -> Self {        Self {            label: label.into(),            mode,            is_selected,        }    }} const BUTTON_LABEL_MAX_WIDTH: f32 = 240.;const MENU_OFFSET_Y: f32 = 4.;const BUTTON_CORNER_RADIUS: f32 = 4.;const BUTTON_VERTICAL_PADDING: f32 = 5.;const BUTTON_HORIZONTAL_PADDING: f32 = 8.; pub struct DiffSelector {    menu: ViewHandle<CodeReviewDiffMenu>,    menu_open: bool,    trigger_mouse_state: MouseStateHandle,    /// Cached label for the trigger button; mirrors the selected `DiffTarget`.    trigger_label: String,} #[derive(Clone, Debug, PartialEq)]pub enum DiffSelectorAction {    Toggle,} #[derive(Clone, Debug)]pub enum DiffSelectorEvent {    SelectMode(DiffMode),} pub fn init(app: &mut AppContext) {    app.register_fixed_bindings([FixedBinding::new(        "enter",        DiffSelectorAction::Toggle,        id!(DiffSelector::ui_name()),    )]);} impl DiffSelector {    pub fn new(ctx: &mut ViewContext<Self>) -> Self {        let menu = ctx.add_typed_action_view(CodeReviewDiffMenu::new);         ctx.subscribe_to_view(&menu, |me, _, event, ctx| match event {            CodeReviewDiffMenuEvent::Select(mode) => {                me.menu_open = false;                ctx.emit(DiffSelectorEvent::SelectMode(mode.clone()));                ctx.notify();            }            CodeReviewDiffMenuEvent::Close => {                me.menu_open = false;                ctx.notify();            }        });         Self {            menu,            menu_open: false,            trigger_mouse_state: MouseStateHandle::default(),            trigger_label: String::new(),        }    }     pub fn toggle(&mut self, ctx: &mut ViewContext<Self>) {        if self.menu_open {            self.close(ctx);        } else {            self.menu_open = true;            self.menu.update(ctx, |menu, ctx| menu.reset(ctx));            ctx.focus(&self.menu);            ctx.notify();        }    }     pub fn close(&mut self, ctx: &mut ViewContext<Self>) {        if self.menu_open {            self.menu_open = false;            ctx.notify();        }    }     /// Replace the menu rows. Trigger label follows the selected target.    pub fn set_targets(&mut self, targets: Vec<DiffTarget>, ctx: &mut ViewContext<Self>) {        self.trigger_label = targets            .iter()            .find(|target| target.is_selected)            .map(|target| target.label.clone())            .unwrap_or_default();        self.menu.update(ctx, |menu, ctx| {            menu.set_targets(targets, ctx);        });        ctx.notify();    }} impl Entity for DiffSelector {    type Event = DiffSelectorEvent;} impl TypedActionView for DiffSelector {    type Action = DiffSelectorAction;     fn handle_action(&mut self, action: &DiffSelectorAction, ctx: &mut ViewContext<Self>) {        match action {            DiffSelectorAction::Toggle => self.toggle(ctx),        }    }} impl View for DiffSelector {    fn ui_name() -> &'static str {        "CodeReviewDiffSelector"    }     fn on_focus(&mut self, focus_ctx: &FocusContext, ctx: &mut ViewContext<Self>) {        if focus_ctx.is_self_focused() && self.menu_open {            ctx.focus(&self.menu);        }    }     fn render(&self, app: &AppContext) -> Box<dyn Element> {        let appearance = Appearance::as_ref(app);        let theme = appearance.theme();        let text_color = theme.main_text_color(theme.background()).into_solid();        let font_family = appearance.ui_font_family();        let font_size = appearance.ui_font_size();         let label = if self.trigger_label.is_empty() {            "Uncommitted changes".to_string()        } else {            self.trigger_label.clone()        };         // Build the icon+text row by hand: `with_text_and_icon_label` wraps        // the text in `Shrinkable<flex=1>`, which makes the button grow to        // fill available width and paints an oversized hover rectangle even        // for short labels.        let icon = ConstrainedBox::new(            Icon::SwitchHorizontal01                .to_warpui_icon(Fill::Solid(text_color))                .finish(),        )        .with_width(15.)        .with_height(15.)        .finish();         // Truncate long branch names. Capping the text directly avoids        // double-constraining padding from an outer wrapper.        let label_text = ConstrainedBox::new(            Text::new_inline(label, font_family, font_size)                .with_color(text_color)                .with_style(Properties::default().weight(Weight::Semibold))                .with_clip(ClipConfig::ellipsis())                .finish(),        )        .with_max_width(BUTTON_LABEL_MAX_WIDTH)        .finish();         let custom_label = Flex::row()            .with_cross_axis_alignment(CrossAxisAlignment::Center)            .with_child(icon)            .with_child(Container::new(label_text).with_margin_left(6.).finish())            .finish();         // Shared hover + active styles so the fill persists when the pointer        // moves into the menu's search input.        let hover_or_active_styles = UiComponentStyles {            background: Some(theme.surface_2().into()),            border_radius: Some(CornerRadius::with_all(Radius::Pixels(BUTTON_CORNER_RADIUS))),            ..Default::default()        };         let mut button = appearance            .ui_builder()            .button(ButtonVariant::Text, self.trigger_mouse_state.clone())            .with_custom_label(custom_label)            .with_style(UiComponentStyles {                font_family_id: Some(font_family),                font_size: Some(font_size),                font_color: Some(text_color),                font_weight: Some(Weight::Semibold),                padding: Some(Coords {                    top: BUTTON_VERTICAL_PADDING,                    bottom: BUTTON_VERTICAL_PADDING,                    left: BUTTON_HORIZONTAL_PADDING,                    right: BUTTON_HORIZONTAL_PADDING,                }),                border_radius: Some(CornerRadius::with_all(Radius::Pixels(BUTTON_CORNER_RADIUS))),                ..Default::default()            })            .with_hovered_styles(hover_or_active_styles)            .with_active_styles(hover_or_active_styles);         if self.menu_open {            button = button.active();        }         let trigger = button            .build()            .with_cursor(Cursor::PointingHand)            .on_click(|ctx, _, _| {                ctx.dispatch_typed_action(DiffSelectorAction::Toggle);            })            .finish();         let mut stack = Stack::new().with_child(trigger);         if self.menu_open {            stack.add_positioned_overlay_child(                ChildView::new(&self.menu).finish(),                OffsetPositioning::offset_from_parent(                    vec2f(0., MENU_OFFSET_Y),                    ParentOffsetBounds::WindowByPosition,                    ParentAnchor::BottomLeft,                    ChildAnchor::TopLeft,                ),            );        }         Container::new(stack.finish()).finish()    }}