259 lines
8.4 KiB
Rust
259 lines
8.4 KiB
Rust
//! Trigger button + [`CodeReviewDiffMenu`] overlay for picking the diff
|
|
//! target in the code review header.
|
|
use pathfinder_geometry::vector::vec2f;
|
|
use galaxy_core::ui::theme::Fill;
|
|
use galaxyui::elements::{
|
|
ChildAnchor, ChildView, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, Element,
|
|
Flex, MouseStateHandle, OffsetPositioning, ParentAnchor, ParentElement, ParentOffsetBounds,
|
|
Radius, Stack, Text,
|
|
};
|
|
use galaxyui::fonts::{Properties, Weight};
|
|
use galaxyui::keymap::FixedBinding;
|
|
use galaxyui::platform::Cursor;
|
|
use galaxyui::text_layout::ClipConfig;
|
|
use galaxyui::ui_components::button::ButtonVariant;
|
|
use galaxyui::ui_components::components::{Coords, UiComponent, UiComponentStyles};
|
|
use galaxyui::{
|
|
id, AppContext, Entity, FocusContext, SingletonEntity as _, TypedActionView, View, ViewContext,
|
|
ViewHandle,
|
|
};
|
|
|
|
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_galaxyui_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()
|
|
}
|
|
}
|