494 lines · 17.8 KB · Rust
RawDownload
mod header_revamp; use pathfinder_geometry::vector::vec2f;use warp_core::features::FeatureFlag;use warpui::elements::{    Align, ChildAnchor, ChildView, Clipped, ConstrainedBox, Container, CrossAxisAlignment, Flex,    Hoverable, MainAxisAlignment, MainAxisSize, MouseStateHandle, OffsetPositioning, ParentAnchor,    ParentElement, ParentOffsetBounds, Shrinkable, SizeConstraintCondition, SizeConstraintSwitch,    Stack,};use warpui::fonts::{Properties, Weight};use warpui::platform::Cursor;use warpui::ui_components::button::{ButtonVariant, TextAndIcon, TextAndIconAlignment};use warpui::ui_components::components::{Coords, UiComponent, UiComponentStyles};use warpui::{AppContext, Element, ModelHandle, ViewHandle}; use crate::appearance::Appearance;use crate::code_review::code_review_view::{    CONTENT_TOP_MARGIN, CodeReviewAction, CodeReviewHeaderFields, CodeReviewView, LoadedState,    get_discard_button_disabled_tooltip,};use crate::code_review::diff_state::DiffStateModel;use crate::menu::Menu;use crate::ui_components::icons::Icon;use crate::view_components::action_button::ActionButton; // This is a best effort guess of the size of all of the elements in the header to know when we should start to wrap to the second rowconst HEADER_WRAP_BREAKPOINT: f32 = 450.; pub(crate) const HEADER_BUTTON_PADDING: Coords = Coords {    top: 2.,    bottom: 2.,    left: 6.,    right: 6.,}; #[derive(Default)]struct StateHandles {    branch_name_tooltip: MouseStateHandle,    discard_all_button: MouseStateHandle,    add_diff_set_context_button: MouseStateHandle,} pub struct CodeReviewHeader {    state_handles: StateHandles,} impl CodeReviewHeader {    pub fn new() -> Self {        Self {            state_handles: StateHandles::default(),        }    }     pub fn render(        &self,        state: &LoadedState,        appearance: &Appearance,        code_review_header_fields: &CodeReviewHeaderFields,        app: &AppContext,    ) -> Box<dyn Element> {        let wide_layout =            self.render_wide_layout(state, appearance, code_review_header_fields, app);         let compact_layout =            self.render_compact_layout(state, appearance, code_review_header_fields, app);         let header_switch = SizeConstraintSwitch::new(            wide_layout,            vec![(                SizeConstraintCondition::WidthLessThan(HEADER_WRAP_BREAKPOINT),                compact_layout,            )],        )        .finish();         Container::new(Clipped::new(Shrinkable::new(1., header_switch).finish()).finish())            .with_margin_top(CONTENT_TOP_MARGIN)            .with_margin_bottom(12.)            .finish()    }     fn render_wide_layout(        &self,        state: &LoadedState,        appearance: &Appearance,        code_review_header_fields: &CodeReviewHeaderFields,        app: &AppContext,    ) -> Box<dyn Element> {        let mut left_section_wide = Flex::row()            .with_main_axis_size(MainAxisSize::Min)            .with_cross_axis_alignment(CrossAxisAlignment::Center);         left_section_wide.add_child(            Shrinkable::new(                100.0,                self.create_branch_tooltip(                    &code_review_header_fields.diff_state_model,                    appearance,                    app,                ),            )            .finish(),        );        left_section_wide.add_child(            Container::new(CodeReviewView::render_diff_stats(                &state.to_diff_stats(),                appearance,            ))            .with_margin_right(8.)            .finish(),        );         let mut right_section_wide = Flex::row()            .with_main_axis_alignment(MainAxisAlignment::End)            .with_cross_axis_alignment(CrossAxisAlignment::Center)            .with_child(ChildView::new(&code_review_header_fields.diff_selector).finish());         let has_no_changes = state.to_diff_stats().has_no_changes();         if FeatureFlag::DiscardPerFileAndAllChanges.is_enabled() {            right_section_wide.add_child(self.create_discard_button(                state,                &code_review_header_fields.diff_state_model,                appearance,                app,            ));        }         if FeatureFlag::DiffSetAsContext.is_enabled() && !has_no_changes {            if FeatureFlag::FileAndDiffSetComments.is_enabled() {                right_section_wide.add_child(self.render_header_dropdown_button(                    &code_review_header_fields.header_dropdown_button,                    &code_review_header_fields.header_menu,                    code_review_header_fields.header_menu_open,                ));            } else {                right_section_wide.add_child(self.render_add_diff_set_context_button(appearance));            }        }         if code_review_header_fields.is_in_split_pane {            right_section_wide = right_section_wide.with_child(self.render_maximize_pane_button(                &code_review_header_fields.maximize_button,                appearance,            ));        }         Clipped::new(            Shrinkable::new(                1.,                Flex::row()                    .with_main_axis_alignment(MainAxisAlignment::SpaceBetween)                    .with_main_axis_size(MainAxisSize::Max)                    .with_cross_axis_alignment(CrossAxisAlignment::Center)                    .with_child(Shrinkable::new(2., left_section_wide.finish()).finish())                    .with_child(right_section_wide.finish())                    .finish(),            )            .finish(),        )        .finish()    }     fn render_compact_layout(        &self,        state: &LoadedState,        appearance: &Appearance,        code_review_header_fields: &CodeReviewHeaderFields,        app: &AppContext,    ) -> Box<dyn Element> {        let mut left_section_compact = Flex::row()            .with_main_axis_size(MainAxisSize::Min)            .with_cross_axis_alignment(CrossAxisAlignment::Center);         left_section_compact.add_child(            Shrinkable::new(                100.0,                self.create_branch_tooltip(                    &code_review_header_fields.diff_state_model,                    appearance,                    app,                ),            )            .finish(),        );        left_section_compact.add_child(            Container::new(CodeReviewView::render_diff_stats(                &state.to_diff_stats(),                appearance,            ))            .with_margin_right(8.)            .finish(),        );         let mut right_subsection_compact = Flex::row()            .with_main_axis_size(MainAxisSize::Min)            .with_cross_axis_alignment(CrossAxisAlignment::Center);         if FeatureFlag::DiscardPerFileAndAllChanges.is_enabled() {            right_subsection_compact.add_child(self.create_discard_button(                state,                &code_review_header_fields.diff_state_model,                appearance,                app,            ));        }         let has_no_changes = state.to_diff_stats().has_no_changes();         if FeatureFlag::DiffSetAsContext.is_enabled() && !has_no_changes {            if FeatureFlag::FileAndDiffSetComments.is_enabled() {                right_subsection_compact.add_child(self.render_header_dropdown_button(                    &code_review_header_fields.header_dropdown_button,                    &code_review_header_fields.header_menu,                    code_review_header_fields.header_menu_open,                ));            } else {                right_subsection_compact                    .add_child(self.render_add_diff_set_context_button(appearance));            }        }         if code_review_header_fields.is_in_split_pane {            right_subsection_compact.add_child(self.render_maximize_pane_button(                &code_review_header_fields.maximize_button,                appearance,            ));        }         let right_section_compact = Flex::row()            .with_main_axis_alignment(MainAxisAlignment::SpaceBetween)            .with_main_axis_size(MainAxisSize::Max)            .with_cross_axis_alignment(CrossAxisAlignment::Center)            .with_child(ChildView::new(&code_review_header_fields.diff_selector).finish())            .with_child(Container::new(right_subsection_compact.finish()).finish());         Clipped::new(            Shrinkable::new(                1.,                Flex::column()                    .with_main_axis_alignment(MainAxisAlignment::Start)                    .with_main_axis_size(MainAxisSize::Min)                    .with_child(Align::new(left_section_compact.finish()).left().finish())                    .with_child(                        Container::new(Align::new(right_section_compact.finish()).right().finish())                            .with_margin_top(8.)                            .finish(),                    )                    .finish(),            )            .finish(),        )        .finish()    }     fn create_branch_name_element(        diff_state_model: &ModelHandle<DiffStateModel>,        appearance: &Appearance,        app: &AppContext,    ) -> Box<dyn Element> {        let theme = appearance.theme();        let header_text = Self::get_header_text(diff_state_model, app);         Container::new(            warpui::elements::Text::new_inline(                header_text,                appearance.ui_font_family(),                appearance.ui_font_size() + 2.,            )            .with_style(Properties::default().weight(Weight::Semibold))            .with_color(theme.main_text_color(theme.background()).into())            .finish(),        )        .with_margin_right(8.)        .finish()    }     fn create_branch_tooltip(        &self,        diff_state_model: &ModelHandle<DiffStateModel>,        appearance: &Appearance,        app: &AppContext,    ) -> Box<dyn Element> {        let header_text = Self::get_header_text(diff_state_model, app);        appearance.ui_builder().overlay_tool_tip_on_element(            header_text,            self.state_handles.branch_name_tooltip.clone(),            Self::create_branch_name_element(diff_state_model, appearance, app),            ParentAnchor::BottomLeft,            ChildAnchor::TopLeft,            vec2f(0., 4.),        )    }     fn create_discard_button(        &self,        state: &LoadedState,        diff_state_model: &ModelHandle<DiffStateModel>,        appearance: &Appearance,        app: &AppContext,    ) -> Box<dyn Element> {        let theme = appearance.theme();        let has_no_changes = state.to_diff_stats().has_no_changes();        let git_operation_blocked = diff_state_model.as_ref(app).is_git_operation_blocked(app);        let is_disabled = has_no_changes || git_operation_blocked;         let sub_text_color = theme.sub_text_color(theme.background());        let mut button_builder = appearance            .ui_builder()            .button(                ButtonVariant::Secondary,                self.state_handles.discard_all_button.clone(),            )            .with_style(UiComponentStyles::default().set_padding(HEADER_BUTTON_PADDING))            .with_style(UiComponentStyles {                font_color: Some(sub_text_color.into()),                ..Default::default()            })            .with_text_and_icon_label(                TextAndIcon::new(                    TextAndIconAlignment::IconFirst,                    "Discard all".to_string(),                    Icon::ReverseLeft.to_warpui_icon(warp_core::ui::theme::Fill::Solid(                        sub_text_color.into_solid(),                    )),                    MainAxisSize::Min,                    MainAxisAlignment::SpaceBetween,                    vec2f(16., 16.),                )                .with_inner_padding(4.),            );         if is_disabled {            let disabled_styles = UiComponentStyles {                font_color: Some(theme.disabled_text_color(theme.background()).into_solid()),                ..Default::default()            };            button_builder = button_builder.with_style(disabled_styles).with_cursor(None);        }         let mut button_hoverable = button_builder.build();         if !is_disabled {            button_hoverable = button_hoverable.on_click(|ctx, _, _| {                ctx.dispatch_typed_action(CodeReviewAction::ShowDiscardConfirmDialog(None));            });        }         let button_element = button_hoverable.finish();         if is_disabled {            let tooltip_text = get_discard_button_disabled_tooltip(git_operation_blocked);            Container::new(CodeReviewHeader::wrap_disabled_button_with_tooltip(                button_element,                tooltip_text,                self.state_handles.discard_all_button.clone(),                appearance,            ))            .with_margin_left(4.)            .finish()        } else {            Container::new(button_element).with_margin_left(4.).finish()        }    }     fn wrap_disabled_button_with_tooltip(        button_element: Box<dyn Element>,        tooltip_text: String,        mouse_state: MouseStateHandle,        appearance: &Appearance,    ) -> Box<dyn Element> {        let ui_builder = appearance.ui_builder();        Hoverable::new(mouse_state, move |state| {            let mut stack = Stack::new().with_child(button_element);            if state.is_hovered() {                let disabled_tooltip = ui_builder.tool_tip(tooltip_text).build().finish();                let tooltip_offset = OffsetPositioning::offset_from_parent(                    vec2f(0., -8.),                    ParentOffsetBounds::WindowByPosition,                    ParentAnchor::TopMiddle,                    ChildAnchor::BottomMiddle,                );                stack.add_positioned_overlay_child(disabled_tooltip, tooltip_offset);            }            stack.finish()        })        .with_cursor(Cursor::Arrow)        .finish()    }     pub(super) fn render_maximize_pane_button(        &self,        maximize_button: &ViewHandle<ActionButton>,        appearance: &Appearance,    ) -> Box<dyn Element> {        Container::new(            ConstrainedBox::new(ChildView::new(maximize_button).finish())                .with_height(appearance.ui_font_size() + 10.)                .with_width(appearance.ui_font_size() + 10.)                .finish(),        )        .with_margin_left(8.)        .with_margin_right(6.)        .finish()    }     fn render_add_diff_set_context_button(&self, appearance: &Appearance) -> Box<dyn Element> {        let theme = appearance.theme();        let ui_builder = appearance.ui_builder().clone();         let button = ui_builder            .button(                ButtonVariant::Secondary,                self.state_handles.add_diff_set_context_button.clone(),            )            .with_text_and_icon_label(TextAndIcon::new(                TextAndIconAlignment::IconFirst,                "",                Icon::Paperclip.to_warpui_icon(warp_core::ui::theme::Fill::Solid(                    theme.main_text_color(theme.background()).into(),                )),                MainAxisSize::Min,                MainAxisAlignment::SpaceBetween,                vec2f(16., 16.),            ))            // manual overrides so it matches the branch dropdown and discard all button            .with_style(UiComponentStyles::default().set_padding(Coords {                top: 6.,                bottom: 6.,                left: 6.,                right: 6.,            }))            .with_tooltip(move || {                ui_builder                    .tool_tip("Add diff set as context".to_owned())                    .build()                    .finish()            })            .with_tooltip_position(warpui::ui_components::button::ButtonTooltipPosition::AboveLeft)            .build()            .on_click(|ctx, _, _| {                ctx.dispatch_typed_action(CodeReviewAction::AddDiffSetAsContext(                    crate::code_review::DiffSetScope::All,                ));            })            .finish();         Container::new(button).with_margin_left(4.).finish()    }     /// Renders the header dropdown trigger    ///    /// This button dispatches a CodeReviewAction and, when the header menu is open, renders the    /// attached menu in a Stack overlay positioned relative to the button (like other overflow    /// buttons in the app, e.g. Drive's "create new" button).    fn render_header_dropdown_button(        &self,        header_dropdown_button: &ViewHandle<ActionButton>,        header_menu: &ViewHandle<Menu<CodeReviewAction>>,        header_menu_open: bool,    ) -> Box<dyn Element> {        let button_container = Container::new(            ConstrainedBox::new(ChildView::new(header_dropdown_button).finish())                .with_height(warp_core::ui::icons::ICON_DIMENSIONS)                .with_width(warp_core::ui::icons::ICON_DIMENSIONS)                .finish(),        )        .with_margin_left(4.)        .finish();         let mut stack = Stack::new().with_child(button_container);         if header_menu_open {            stack.add_positioned_overlay_child(                ChildView::new(header_menu).finish(),                OffsetPositioning::offset_from_parent(                    vec2f(0., 0.),                    ParentOffsetBounds::WindowByPosition,                    ParentAnchor::BottomRight,                    ChildAnchor::TopRight,                ),            );        }         stack.finish()    }     fn get_header_text(diff_state_model: &ModelHandle<DiffStateModel>, app: &AppContext) -> String {        let branch_name =            diff_state_model.read(app, |model, ctx| model.get_current_branch_name(ctx));        branch_name.unwrap_or("Reviewing open changes".to_string())    }}