first pass of merging in warp (doesn't build)

This commit is contained in:
Ryan Ward
2026-07-01 16:08:58 -05:00
parent 2f64909469
commit 4770ac06b5
3662 changed files with 414574 additions and 89772 deletions
+36 -26
View File
@@ -1,33 +1,34 @@
use galaxy_core::ui::{
appearance::Appearance,
color::{coloru_with_opacity, contrast::MinimumAllowedContrast, ContrastingColor},
theme::{color::internal_colors, AnsiColorIdentifier, Fill},
};
use galaxyui::{elements::MainAxisAlignment, Gradient};
use galaxyui::{elements::MainAxisSize, text_layout::ClipConfig};
use galaxyui::{
elements::{
Border, ChildAnchor, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, Flex,
Hoverable, MouseStateHandle, OffsetPositioning, Padding, ParentAnchor, ParentElement as _,
ParentOffsetBounds, Radius, Stack, Text, DEFAULT_UI_LINE_HEIGHT_RATIO,
},
fonts::{Properties, Weight},
keymap::Keystroke,
platform::Cursor,
ui_components::components::{Coords, UiComponent, UiComponentStyles},
AppContext, BlurContext, Element, Entity, EventContext, FocusContext, SingletonEntity as _,
TypedActionView, View, ViewContext,
};
use std::borrow::Cow;
use std::sync::Arc;
use pathfinder_color::ColorU;
use pathfinder_geometry::vector::{vec2f, Vector2F};
use galaxy_core::ui::appearance::Appearance;
use galaxy_core::ui::color::contrast::MinimumAllowedContrast;
use galaxy_core::ui::color::{coloru_with_opacity, ContrastingColor};
use galaxy_core::ui::theme::color::internal_colors;
use galaxy_core::ui::theme::{AnsiColorIdentifier, Fill};
use galaxyui::elements::{
Border, ChildAnchor, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, Flex,
Hoverable, MainAxisAlignment, MainAxisSize, MouseStateHandle, OffsetPositioning, Padding,
ParentAnchor, ParentElement as _, ParentOffsetBounds, Radius, Stack, Text,
DEFAULT_UI_LINE_HEIGHT_RATIO,
};
use galaxyui::fonts::{Properties, Weight};
use galaxyui::keymap::Keystroke;
use galaxyui::platform::Cursor;
use galaxyui::text_layout::ClipConfig;
use galaxyui::ui_components::components::{Coords, UiComponent, UiComponentStyles};
use galaxyui::{
AppContext, BlurContext, Element, Entity, EventContext, FocusContext, Gradient,
SingletonEntity as _, TypedActionView, View, ViewContext,
};
use std::{borrow::Cow, sync::Arc};
use crate::{
settings_view::keybindings::{KeybindingChangedEvent, KeybindingChangedNotifier},
terminal::input::{MenuPositioning, MenuPositioningProvider},
ui_components::icons::Icon,
util::bindings::keybinding_name_to_keystroke,
};
use crate::settings_view::keybindings::{KeybindingChangedEvent, KeybindingChangedNotifier};
use crate::terminal::input::{MenuPositioning, MenuPositioningProvider};
use crate::ui_components::icons::Icon;
use crate::util::bindings::keybinding_name_to_keystroke;
/// Maximum width of a tooltip before it soft-wraps.
const TOOLTIP_MAX_WIDTH: f32 = 300.;
@@ -324,6 +325,10 @@ impl ActionButton {
self.has_menu = has_menu;
self
}
pub fn set_has_menu(&mut self, has_menu: bool, ctx: &mut ViewContext<Self>) {
self.has_menu = has_menu;
ctx.notify();
}
#[allow(dead_code)]
pub fn with_callout(mut self, callout: Callout) -> Self {
@@ -432,6 +437,11 @@ impl ActionButton {
ctx.notify();
}
pub fn clear_tooltip(&mut self, ctx: &mut ViewContext<Self>) {
self.tooltip = None;
ctx.notify();
}
pub fn set_tooltip_sublabel(
&mut self,
tooltip_sublabel: Option<impl Into<String>>,
+15 -16
View File
@@ -1,23 +1,21 @@
use std::time::Duration;
use galaxyui::elements::{DropShadow, Expanded};
use galaxyui::r#async::Timer;
use galaxyui::WindowId;
use galaxyui::{
elements::{
ChildAnchor, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment,
DispatchEventResult, EventHandler, Flex, Hoverable, Icon, MouseStateHandle,
OffsetPositioning, Padding, ParentElement, PositionedElementAnchor,
PositionedElementOffsetBounds, Radius, SavePosition, Stack,
},
keymap::Keystroke,
r#async::SpawnedFutureHandle,
ui_components::components::{Coords, UiComponent, UiComponentStyles},
AppContext, Element, Entity, EntityId, SingletonEntity, TypedActionView, View, ViewContext,
};
use pathfinder_color::ColorU;
use pathfinder_geometry::vector::vec2f;
use uuid::Uuid;
use galaxyui::elements::{
ChildAnchor, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, DispatchEventResult,
DropShadow, EventHandler, Expanded, Flex, Hoverable, Icon, MouseStateHandle, OffsetPositioning,
Padding, ParentElement, PositionedElementAnchor, PositionedElementOffsetBounds, Radius,
SavePosition, Stack,
};
use galaxyui::keymap::Keystroke;
use galaxyui::r#async::{SpawnedFutureHandle, Timer};
use galaxyui::ui_components::components::{Coords, UiComponent, UiComponentStyles};
use galaxyui::{
AppContext, Element, Entity, EntityId, SingletonEntity, TypedActionView, View, ViewContext,
WindowId,
};
use crate::appearance::Appearance;
use crate::settings_view::keybindings::{KeybindingChangedEvent, KeybindingChangedNotifier};
@@ -433,9 +431,10 @@ impl AgentToast {
appearance: &Appearance,
keystroke: Keystroke,
) -> Box<dyn Element> {
use crate::ui_components::blended_colors;
use galaxyui::ui_components::keyboard_shortcut::KeyboardShortcut;
use crate::ui_components::blended_colors;
let theme = appearance.theme();
let keybinding_style = UiComponentStyles {
+9 -10
View File
@@ -1,15 +1,14 @@
use galaxy_core::ui::theme::color::internal_colors;
use galaxyui::{
elements::{
Border, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, Flex, Icon,
MainAxisSize, ParentElement, Radius, Shrinkable,
},
ui_components::components::{UiComponent, UiComponentStyles},
Element,
};
use pathfinder_color::ColorU;
use galaxy_core::ui::theme::color::internal_colors;
use galaxyui::elements::{
Border, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, Flex, Icon, MainAxisSize,
ParentElement, Radius, Shrinkable,
};
use galaxyui::ui_components::components::{UiComponent, UiComponentStyles};
use galaxyui::Element;
use crate::{appearance::Appearance, themes::theme::Fill};
use crate::appearance::Appearance;
use crate::themes::theme::Fill;
const ALERT_CORNER_RADIUS: f32 = 4.;
const ALERT_VERTICAL_PADDING: f32 = 8.;
+3 -2
View File
@@ -1,4 +1,6 @@
use galaxy_core::ui::theme::{phenomenon::PhenomenonStyle, Fill};
use pathfinder_color::ColorU;
use galaxy_core::ui::theme::phenomenon::PhenomenonStyle;
use galaxy_core::ui::theme::Fill;
use galaxyui::elements::{
Border, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, Flex, MainAxisSize,
MouseStateHandle, ParentElement, Radius, Stack,
@@ -6,7 +8,6 @@ use galaxyui::elements::{
use galaxyui::ui_components::checkbox::Checkbox;
use galaxyui::ui_components::components::UiComponentStyles;
use galaxyui::Element;
use pathfinder_color::ColorU;
use crate::appearance::Appearance;
use crate::ui_components::icons::Icon;
@@ -1,20 +1,16 @@
#![allow(dead_code, unused_imports, unused_variables)]
use crate::{
appearance::Appearance,
editor::{EditorOptions, EditorView, Event as EditorEvent, TextOptions},
};
use galaxyui::elements::{Container, CornerRadius, Dismiss, MouseStateHandle, Radius};
use galaxyui::fonts::Weight;
use galaxyui::platform::Cursor;
use galaxyui::ui_components::button::ButtonVariant;
use galaxyui::ui_components::components::{UiComponent, UiComponentStyles};
use galaxyui::{
elements::{Container, CornerRadius, Dismiss, MouseStateHandle, Radius},
fonts::Weight,
platform::Cursor,
ui_components::{
button::ButtonVariant,
components::{UiComponent, UiComponentStyles},
},
AppContext, Element, Entity, FocusContext, SingletonEntity, TypedActionView, View, ViewContext,
ViewHandle,
};
use crate::appearance::Appearance;
use crate::editor::{EditorOptions, EditorView, Event as EditorEvent, TextOptions};
/// This View is a text that can be hovered over. Upon clicking,
/// the text becomes a text input that can be submitted
/// by hitting enter or clicking outside the input.
+41 -36
View File
@@ -1,25 +1,24 @@
use super::dropdown::DropdownAction;
use crate::{
appearance::Appearance,
menu::{Event as MenuEvent, Menu, MenuItem, MenuItemFields, MenuVariant},
themes::theme::Fill,
ui_components::icons::Icon,
};
use galaxyui::{
elements::{
Border, ChildAnchor, ConstrainedBox, CornerRadius, CrossAxisAlignment, Flex,
Icon as WarpUiIcon, MainAxisAlignment, MouseStateHandle, OffsetPositioning, ParentElement,
PositionedElementAnchor, PositionedElementOffsetBounds, Radius, SavePosition, Stack,
},
presenter::ChildView,
ui_components::{
button::ButtonVariant,
components::{Coords, UiComponent, UiComponentStyles},
},
Action, AppContext, BlurContext, Element, Entity, SingletonEntity, TypedActionView, View,
ViewContext, ViewHandle,
};
use std::marker::PhantomData;
use pathfinder_geometry::vector::vec2f;
use galaxyui::elements::{
Border, ChildAnchor, ConstrainedBox, CornerRadius, CrossAxisAlignment, Flex,
Icon as WarpUiIcon, MainAxisAlignment, MouseStateHandle, OffsetPositioning, ParentElement,
PositionedElementAnchor, PositionedElementOffsetBounds, Radius, SavePosition, Stack,
};
use galaxyui::presenter::ChildView;
use galaxyui::ui_components::button::ButtonVariant;
use galaxyui::ui_components::components::{Coords, UiComponent, UiComponentStyles};
use galaxyui::{
AppContext, BlurContext, Element, Entity, SingletonEntity, TypedActionView, View, ViewContext,
ViewHandle,
};
use super::dropdown::{DropdownAction, DropdownItemAction};
use crate::appearance::Appearance;
use crate::menu::{Event as MenuEvent, Menu, MenuItem, MenuItemFields, MenuVariant};
use crate::themes::theme::Fill;
use crate::ui_components::icons::Icon;
#[cfg(test)]
#[path = "compact_dropdown_tests.rs"]
@@ -30,18 +29,19 @@ mod tests;
///
/// This is useful instead of [`crate::dropdown::Dropdown`] when showing a
/// dropdown alongside other controls, such as in a formatting UI.
pub struct CompactDropdown<A: Action + Clone> {
pub struct CompactDropdown<A: DropdownItemAction = ()> {
/// Whether the dropdown is open.
is_expanded: bool,
/// Mouse state for the dropdown button.
top_bar_mouse_state: MouseStateHandle,
/// Dropdown menu.
dropdown: ViewHandle<Menu<DropdownAction<A>>>,
dropdown: ViewHandle<Menu<DropdownAction>>,
/// The size that icons are scaled to. Defaults to the UI font size if not set.
icon_size: Option<f32>,
_action_type: PhantomData<A>,
}
pub struct CompactDropdownItem<A: Action + Clone> {
pub struct CompactDropdownItem<A: DropdownItemAction = ()> {
/// Icon identifier for this item.
icon: Icon,
/// Optional override color for the icon.
@@ -52,7 +52,7 @@ pub struct CompactDropdownItem<A: Action + Clone> {
action: A,
}
impl<A: Action + Clone> CompactDropdown<A> {
impl<A: DropdownItemAction> CompactDropdown<A> {
/// Create a new, empty compact dropdown. The [`MenuVariant`] determines whether or not the
/// dropdown is scrollable when expanded.
pub fn new(menu_variant: MenuVariant, ctx: &mut ViewContext<Self>) -> Self {
@@ -73,6 +73,7 @@ impl<A: Action + Clone> CompactDropdown<A> {
dropdown,
top_bar_mouse_state: Default::default(),
icon_size: None,
_action_type: PhantomData,
}
}
@@ -163,7 +164,7 @@ impl<A: Action + Clone> CompactDropdown<A> {
// if the dropdown is not expanded.
if !self.is_expanded {
top_bar = top_bar.on_click(|ctx, _, _| {
ctx.dispatch_typed_action(DropdownAction::<A>::ToggleExpanded);
ctx.dispatch_typed_action(DropdownAction::ToggleExpanded);
});
}
@@ -204,7 +205,11 @@ impl<A: Action + Clone> CompactDropdown<A> {
/// Adapter between [`MenuItem`] click callbacks and the parent action type.
/// When a dropdown menu item is selected, we dispatch its action and close
/// the dropdown.
fn select_action_and_close(&mut self, action: &A, ctx: &mut ViewContext<Self>) {
fn select_action_and_close(
&mut self,
action: &dyn DropdownItemAction,
ctx: &mut ViewContext<Self>,
) {
ctx.dispatch_typed_action(action);
self.close(ctx);
}
@@ -217,11 +222,11 @@ impl<A: Action + Clone> CompactDropdown<A> {
}
}
impl<A: Action + Clone> Entity for CompactDropdown<A> {
impl<A: DropdownItemAction> Entity for CompactDropdown<A> {
type Event = CompactDropdownEvent;
}
impl<A: Action + Clone> View for CompactDropdown<A> {
impl<A: DropdownItemAction> View for CompactDropdown<A> {
fn ui_name() -> &'static str {
"CompactDropdown"
}
@@ -252,22 +257,22 @@ impl<A: Action + Clone> View for CompactDropdown<A> {
}
}
impl<A: Action + Clone> TypedActionView for CompactDropdown<A> {
type Action = DropdownAction<A>;
impl<A: DropdownItemAction> TypedActionView for CompactDropdown<A> {
type Action = DropdownAction;
fn handle_action(&mut self, action: &DropdownAction<A>, ctx: &mut ViewContext<Self>) {
fn handle_action(&mut self, action: &DropdownAction, ctx: &mut ViewContext<Self>) {
match action {
DropdownAction::Focus(_) => self.focus(ctx),
DropdownAction::Close => self.close(ctx),
DropdownAction::SelectActionAndClose(action) => {
self.select_action_and_close(action, ctx)
self.select_action_and_close(action.as_ref(), ctx)
}
DropdownAction::ToggleExpanded => self.toggle_expanded(ctx),
}
}
}
impl<A: Action + Clone> CompactDropdownItem<A> {
impl<A: DropdownItemAction> CompactDropdownItem<A> {
pub fn new(icon: Icon, display_text: impl Into<String>, action: A) -> Self {
Self {
display_text: display_text.into(),
@@ -284,10 +289,10 @@ impl<A: Action + Clone> CompactDropdownItem<A> {
self
}
fn menu_item(self) -> MenuItem<DropdownAction<A>> {
fn menu_item(self) -> MenuItem<DropdownAction> {
let mut item = MenuItemFields::new(self.display_text)
.with_icon(self.icon)
.with_on_select_action(DropdownAction::SelectActionAndClose(self.action));
.with_on_select_action(DropdownAction::SelectActionAndClose(Box::new(self.action)));
if let Some(color) = self.icon_color {
item = item.with_override_icon_color(color);
}
@@ -1,11 +1,12 @@
use galaxy_core::ui::appearance::Appearance;
use galaxyui::{platform::WindowStyle, App, View};
use crate::{menu::MenuVariant, ui_components::icons::Icon};
use galaxyui::platform::WindowStyle;
use galaxyui::{App, View};
use super::{CompactDropdown, CompactDropdownItem};
use crate::menu::MenuVariant;
use crate::ui_components::icons::Icon;
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq)]
struct TestAction;
/// Baseline test that the view can render.
@@ -1,19 +1,16 @@
use std::sync::Arc;
use crate::{
ai::blocklist::inline_action::inline_action_icons::icon_size,
ui_components::icons::Icon,
view_components::action_button::{
ActionButton, ActionButtonTheme, AdjoinedSide, ButtonSize, KeystrokeSource,
},
};
use galaxy_core::ui::appearance::Appearance;
use galaxyui::{
elements::{
ChildView, ConstrainedBox, Container, CrossAxisAlignment, Flex, MainAxisAlignment,
MainAxisSize, ParentElement,
},
Action, AppContext, Element, TypedActionView, View, ViewContext, ViewHandle,
use galaxyui::elements::{
ChildView, ConstrainedBox, Container, CrossAxisAlignment, Flex, MainAxisAlignment,
MainAxisSize, ParentElement,
};
use galaxyui::{Action, AppContext, Element, TypedActionView, View, ViewContext, ViewHandle};
use crate::ai::blocklist::inline_action::inline_action_icons::icon_size;
use crate::ui_components::icons::Icon;
use crate::view_components::action_button::{
ActionButton, ActionButtonTheme, AdjoinedSide, ButtonSize, KeystrokeSource,
};
const BUTTON_MARGIN: f32 = 8.;
@@ -127,6 +124,30 @@ impl CompactibleActionButton {
});
}
/// Sets the disabled state on both the compact and expanded buttons.
pub fn set_disabled<T: View>(&mut self, disabled: bool, ctx: &mut ViewContext<T>) {
self.compact_button.update(ctx, |button, ctx| {
button.set_disabled(disabled, ctx);
});
self.expanded_button.update(ctx, |button, ctx| {
button.set_disabled(disabled, ctx);
});
}
/// Sets the tooltip on both buttons. Passing `None` clears the compact
/// button's hover affordance; use [`set_label`](Self::set_label) to
/// preserve the label tooltip.
pub fn set_tooltip<T: View>(&mut self, tooltip: Option<String>, ctx: &mut ViewContext<T>) {
let compact_tooltip = tooltip.clone();
let expanded_tooltip = tooltip;
self.compact_button.update(ctx, |button, ctx| {
button.set_tooltip(compact_tooltip, ctx);
});
self.expanded_button.update(ctx, |button, ctx| {
button.set_tooltip(expanded_tooltip, ctx);
});
}
pub fn compact_button(&self) -> &ViewHandle<ActionButton> {
&self.compact_button
}
@@ -3,15 +3,13 @@ use std::sync::Arc;
use galaxyui::elements::{ChildView, Flex, ParentElement, SavePosition};
use galaxyui::{Action, Element, TypedActionView, View, ViewContext, ViewHandle};
use crate::view_components::action_button::AdjoinedSide;
use crate::view_components::compactible_action_button::RenderCompactibleActionButton;
use crate::{
ui_components::icons::Icon,
view_components::action_button::{
ActionButton, ButtonSize, KeystrokeSource, NakedTheme, PrimaryRightBiasedTheme,
PrimaryTheme,
},
view_components::compactible_action_button::CompactibleActionButton,
use crate::ui_components::icons::Icon;
use crate::view_components::action_button::{
ActionButton, AdjoinedSide, ButtonSize, KeystrokeSource, NakedTheme, PrimaryRightBiasedTheme,
PrimaryTheme,
};
use crate::view_components::compactible_action_button::{
CompactibleActionButton, RenderCompactibleActionButton,
};
/// A split button composed of a primary CompactibleActionButton and a trailing
@@ -107,6 +105,19 @@ impl CompactibleSplitActionButton {
) {
self.primary_button.set_keybinding(keybinding, ctx);
}
/// Sets the disabled state on both the primary and menu buttons.
pub fn set_disabled<T: View>(&mut self, disabled: bool, ctx: &mut ViewContext<T>) {
self.primary_button.set_disabled(disabled, ctx);
self.menu_button.update(ctx, |button, ctx| {
button.set_disabled(disabled, ctx);
});
}
/// Sets the tooltip on the primary button.
pub fn set_tooltip<T: View>(&mut self, tooltip: Option<String>, ctx: &mut ViewContext<T>) {
self.primary_button.set_tooltip(tooltip, ctx);
}
}
impl RenderCompactibleActionButton for CompactibleSplitActionButton {
@@ -1,6 +1,10 @@
//! A reusable component for displaying text with a copy button that shows
//! checkmark feedback when clicked.
use std::time::Duration;
use instant::Instant;
use galaxyui::color::ColorU;
use galaxyui::elements::{
ConstrainedBox, Container, CrossAxisAlignment, Element, Expanded, Flex, MouseStateHandle,
ParentElement, Shrinkable, Text,
@@ -8,12 +12,9 @@ use galaxyui::elements::{
use galaxyui::text_layout::ClipConfig;
use galaxyui::ui_components::components::UiComponent;
use galaxyui::{AppContext, SingletonEntity};
use instant::Instant;
use std::time::Duration;
use crate::appearance::Appearance;
use crate::ui_components::icons::Icon;
use galaxyui::color::ColorU;
/// Duration to show the checkmark after copying.
pub const COPY_FEEDBACK_DURATION: Duration = Duration::from_secs(2);
+18 -20
View File
@@ -1,31 +1,29 @@
use std::rc::Rc;
use std::time::Duration;
use galaxy_core::ui::builder::UiBuilder;
use galaxy_core::ui::theme::color::internal_colors;
use galaxyui::elements::ChildView;
use galaxyui::keymap::Keystroke;
use galaxyui::r#async::Timer;
use galaxyui::{
elements::{
Border, ChildAnchor, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment,
DispatchEventResult, EventHandler, Flex, Hoverable, Icon, MainAxisAlignment, MainAxisSize,
MouseStateHandle, OffsetPositioning, ParentElement, PositionedElementAnchor,
PositionedElementOffsetBounds, Radius, SavePosition, Shrinkable, Stack,
},
fonts::Weight,
r#async::SpawnedFutureHandle,
ui_components::components::{Coords, UiComponent, UiComponentStyles},
AppContext, Element, Entity, SingletonEntity, TypedActionView, View, ViewContext,
};
use galaxyui::{Action, ViewHandle};
use pathfinder_color::ColorU;
use pathfinder_geometry::vector::vec2f;
use uuid::Uuid;
use crate::{appearance::Appearance, themes::theme::Fill};
use galaxy_core::ui::builder::UiBuilder;
use galaxy_core::ui::theme::color::internal_colors;
use galaxyui::elements::{
Border, ChildAnchor, ChildView, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment,
DispatchEventResult, EventHandler, Flex, Hoverable, Icon, MainAxisAlignment, MainAxisSize,
MouseStateHandle, OffsetPositioning, ParentElement, PositionedElementAnchor,
PositionedElementOffsetBounds, Radius, SavePosition, Shrinkable, Stack,
};
use galaxyui::fonts::Weight;
use galaxyui::keymap::Keystroke;
use galaxyui::r#async::{SpawnedFutureHandle, Timer};
use galaxyui::ui_components::components::{Coords, UiComponent, UiComponentStyles};
use galaxyui::{
Action, AppContext, Element, Entity, SingletonEntity, TypedActionView, View, ViewContext,
ViewHandle,
};
use super::action_button::ActionButton;
use crate::appearance::Appearance;
use crate::themes::theme::Fill;
const TOAST_WIDTH: f32 = 464.;
const TOAST_CORNER_RADIUS: f32 = 4.;
+229 -65
View File
@@ -1,33 +1,63 @@
use std::fmt::Debug;
use std::marker::PhantomData;
use pathfinder_color::ColorU;
use galaxyui::elements::{
Border, ChildAnchor, ChildView, ConstrainedBox, Container, CornerRadius, Element, Fill, Icon,
MainAxisAlignment, MainAxisSize, MouseStateHandle, OffsetPositioning, ParentElement,
PositionedElementAnchor, PositionedElementOffsetBounds, SavePosition, Stack,
};
use galaxyui::fonts::FamilyId;
use galaxyui::geometry::vector::vec2f;
use galaxyui::scene::DropShadow;
use galaxyui::text_layout::ClipConfig;
use galaxyui::ui_components::button::{ButtonVariant, TextAndIcon, TextAndIconAlignment};
use galaxyui::ui_components::components::{Coords, UiComponent, UiComponentStyles};
use galaxyui::{
elements::{
Border, ChildAnchor, ChildView, ConstrainedBox, Container, Element, Icon,
MainAxisAlignment, MainAxisSize, MouseStateHandle, OffsetPositioning, ParentElement,
PositionedElementAnchor, PositionedElementOffsetBounds, SavePosition, Stack,
},
fonts::FamilyId,
geometry::vector::vec2f,
scene::DropShadow,
ui_components::{
button::{ButtonVariant, TextAndIcon, TextAndIconAlignment},
components::{Coords, UiComponent, UiComponentStyles},
},
Action, AppContext, BlurContext, Entity, SingletonEntity, TypedActionView, View, ViewContext,
ViewHandle, WeakViewHandle,
};
use pathfinder_color::ColorU;
use crate::{
appearance::Appearance,
menu::{Event as MenuEvent, Menu, MenuItem, MenuItemFields, MenuVariant},
};
use crate::appearance::Appearance;
use crate::menu::{Event as MenuEvent, Menu, MenuItem, MenuItemFields, MenuVariant};
pub const TOP_MENU_BAR_HEIGHT: f32 = 30.;
pub const TOP_MENU_BAR_MAX_WIDTH: f32 = 190.;
pub const DROPDOWN_PADDING: f32 = 6.;
pub type MenuHeaderTextFormatter = Box<dyn Fn(&str) -> String>;
pub trait DropdownItemAction: Action {
fn clone_box(&self) -> Box<dyn DropdownItemAction>;
fn eq_action(&self, other: &dyn DropdownItemAction) -> bool;
}
impl<T> DropdownItemAction for T
where
T: Action + Clone + PartialEq + 'static,
{
fn clone_box(&self) -> Box<dyn DropdownItemAction> {
Box::new(self.clone())
}
fn eq_action(&self, other: &dyn DropdownItemAction) -> bool {
other
.as_any()
.downcast_ref::<T>()
.is_some_and(|other| self == other)
}
}
impl Clone for Box<dyn DropdownItemAction> {
fn clone(&self) -> Self {
self.as_ref().clone_box()
}
}
impl PartialEq for Box<dyn DropdownItemAction> {
fn eq(&self, other: &Self) -> bool {
self.as_ref().eq_action(other.as_ref())
}
}
#[derive(Clone, Default)]
pub enum DropdownStyle {
@@ -63,7 +93,12 @@ impl DropdownStyle {
/// A dropdown menu view. The view renders each DropdownItem. When a menu item is clicked,
/// on_click_action_name is dispatched, with the value of the corresponding menu item.
pub struct Dropdown<A: Action + Clone> {
///
/// The item action type powers typed item helpers like `set_items` and
/// `set_selected_by_action`. Callers that populate the menu with already-erased rich
/// [`MenuItem<DropdownAction>`] values through `set_rich_items` do not need to name a concrete
/// item action type.
pub struct Dropdown<A: DropdownItemAction = ()> {
is_expanded: bool,
disabled: bool,
top_bar_mouse_state: MouseStateHandle,
@@ -72,8 +107,8 @@ pub struct Dropdown<A: Action + Clone> {
child_anchor: ChildAnchor,
main_axis_size: MainAxisSize,
dropdown: ViewHandle<Menu<DropdownAction<A>>>,
selected_item: Option<MenuItem<DropdownAction<A>>>,
dropdown: ViewHandle<Menu<DropdownAction>>,
selected_item: Option<MenuItem<DropdownAction>>,
// Function for overriding the default closed-state text (the selected item)
menu_header_text_override: Option<MenuHeaderTextFormatter>,
self_handle: WeakViewHandle<Self>,
@@ -82,23 +117,60 @@ pub struct Dropdown<A: Action + Clone> {
font_color: Option<ColorU>,
font_size: Option<f32>,
padding: Option<Coords>,
/// Optional override for the top-bar background fill, applied on top
/// of the variant's default style. Used by callers that need a
/// per-call appearance distinct from the shared `DropdownStyle`
/// variants (e.g. orchestrate confirmation card pickers per Figma
/// 4340:117057).
background: Option<Fill>,
/// Optional override for the top-bar border fill. See `background`.
border_color: Option<Fill>,
/// Optional override for the top-bar border width.
border_width: Option<f32>,
/// Optional override for the top-bar corner radius.
border_radius: Option<CornerRadius>,
vertical_margin: f32,
top_bar_height: f32,
/// When true (default), the open menu is attached to the dropdown's
/// stack via `add_positioned_overlay_child`, painting it in an
/// `Overlay` layer that escapes parent clip bounds. When false, the
/// menu is attached via `add_positioned_child` and paints in the
/// parent's Normal layer, the same way other AIBlock-internal
/// menus (e.g. the accept-and-autoexecute split-button menu in
/// `requested_command.rs` / `code_diff_view.rs`) do.
///
/// Setting this to `false` is required for dropdowns rendered
/// inside a `SelectableArea` whose menu items would otherwise lose
/// `LeftMouseDown` / `LeftMouseUp` (hover still works) due to an
/// interaction between `Menu`'s `prevent_interaction_with_other_elements`
/// full-window hit-recording rect and the surrounding
/// `SelectableArea`. Tracked as P1.1 for the orchestrate
/// confirmation card pickers.
use_overlay_layer: bool,
match_menu_width_to_top_bar: bool,
// The menu stores erased `DropdownAction`s internally, but the public API remains generic over
// the caller's concrete item action type (`set_items`, `set_selected_by_action`, etc.).
_item_action_type: PhantomData<A>,
}
#[derive(Clone)]
pub struct DropdownItem<A: Action + Clone> {
pub struct DropdownItem<A: DropdownItemAction = ()> {
/// Text to display for the item
pub display_text: String,
/// Constructor for the typed action object
action: A,
/// Custom font for the dropdown item
family_id: Option<FamilyId>,
/// Optional hover tooltip shown over the row.
tooltip: Option<String>,
/// Optional clip config controlling how `display_text` is clipped when it
/// would overflow the row width. Forwarded to [`MenuItemFields`].
clip_config: Option<ClipConfig>,
}
impl<A> DropdownItem<A>
where
A: Action + Clone,
A: DropdownItemAction,
{
pub fn new<S>(display_text: S, action: A) -> Self
where
@@ -108,6 +180,8 @@ where
display_text: display_text.into(),
action,
family_id: None,
tooltip: None,
clip_config: None,
}
}
@@ -117,17 +191,38 @@ where
self.family_id = Some(family_id);
self
}
/// Set a hover tooltip for this row. Useful when `display_text` is a
/// shortened form of richer underlying data (e.g. a truncated path).
pub fn with_tooltip(mut self, tooltip: impl Into<String>) -> Self {
self.tooltip = Some(tooltip.into());
self
}
/// Set a [`ClipConfig`] for this row. When set, the dropdown's text-layout
/// layer clips `display_text` at the actual rendered width instead of
/// callers having to pre-shrink the string.
pub fn with_clip_config(mut self, config: ClipConfig) -> Self {
self.clip_config = Some(config);
self
}
}
impl<A> From<&DropdownItem<A>> for MenuItem<DropdownAction<A>>
impl<A> From<&DropdownItem<A>> for MenuItem<DropdownAction>
where
A: Action + Clone,
A: DropdownItemAction,
{
fn from(dropdown_item: &DropdownItem<A>) -> MenuItem<DropdownAction<A>> {
let menu_item = MenuItemFields::new(dropdown_item.display_text.clone())
fn from(dropdown_item: &DropdownItem<A>) -> MenuItem<DropdownAction> {
let mut menu_item = MenuItemFields::new(dropdown_item.display_text.clone())
.with_on_select_action(DropdownAction::SelectActionAndClose(
dropdown_item.action.clone(),
dropdown_item.action.clone_box(),
));
if let Some(tooltip) = &dropdown_item.tooltip {
menu_item = menu_item.with_tooltip(tooltip.clone());
}
if let Some(clip_config) = dropdown_item.clip_config {
menu_item = menu_item.with_clip_config(clip_config);
}
if let Some(family_id) = dropdown_item.family_id {
menu_item.with_font_override(family_id).into_item()
} else {
@@ -136,23 +231,29 @@ where
}
}
impl<A> From<A> for DropdownAction<A>
where
A: Action + Clone,
{
fn from(action: A) -> DropdownAction<A> {
DropdownAction::SelectActionAndClose(action)
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum DropdownAction<A: Action + Clone> {
pub enum DropdownAction {
Focus(usize),
Close,
SelectActionAndClose(A),
SelectActionAndClose(Box<dyn DropdownItemAction>),
ToggleExpanded,
}
impl DropdownAction {
/// Wraps a caller item action so menu selection can close the dropdown before dispatching the
/// typed action.
///
/// This is an inherent constructor instead of `From<A>` because a blanket
/// `impl<A> From<A> for DropdownAction` would overlap with Rust's `impl<T> From<T> for T`
/// when `A = DropdownAction`.
pub fn select_action_and_close<A>(action: A) -> Self
where
A: DropdownItemAction,
{
Self::SelectActionAndClose(Box::new(action))
}
}
pub enum DropdownEvent {
ToggleExpanded,
Close,
@@ -160,7 +261,7 @@ pub enum DropdownEvent {
impl<A> Dropdown<A>
where
A: Action + Clone,
A: DropdownItemAction,
{
pub fn new(ctx: &mut ViewContext<Self>) -> Self {
let dropdown = ctx.add_typed_action_view(|ctx| {
@@ -191,11 +292,42 @@ where
font_color: None,
font_size: None,
padding: None,
background: None,
border_color: None,
border_width: None,
border_radius: None,
vertical_margin: DROPDOWN_PADDING,
top_bar_height: TOP_MENU_BAR_HEIGHT,
use_overlay_layer: true,
match_menu_width_to_top_bar: false,
_item_action_type: PhantomData,
}
}
/// Controls whether the open menu is rendered in an `Overlay`
/// layer (default) or attached as a positioned child in the
/// dropdown stack's Normal layer. See the field-level docs on
/// `use_overlay_layer` for when each is appropriate.
pub fn set_use_overlay_layer(&mut self, use_overlay_layer: bool, ctx: &mut ViewContext<Self>) {
self.use_overlay_layer = use_overlay_layer;
ctx.notify();
}
pub fn set_background(&mut self, background: Fill, ctx: &mut ViewContext<Self>) {
self.background = Some(background);
ctx.notify();
}
pub fn set_border_width(&mut self, border_width: f32, ctx: &mut ViewContext<Self>) {
self.border_width = Some(border_width);
ctx.notify();
}
pub fn set_border_radius(&mut self, border_radius: CornerRadius, ctx: &mut ViewContext<Self>) {
self.border_radius = Some(border_radius);
ctx.notify();
}
pub fn with_drop_shadow(mut self) -> Self {
self.use_drop_shadow = true;
self
@@ -263,6 +395,22 @@ where
ctx.notify();
}
/// When enabled, the open menu sizes itself to the last rendered width of
/// the dropdown's top bar. This is useful for flexible dropdowns whose
/// trigger width is determined by parent layout rather than a fixed max.
pub fn set_match_menu_width_to_top_bar(
&mut self,
match_width: bool,
ctx: &mut ViewContext<Self>,
) {
self.match_menu_width_to_top_bar = match_width;
let top_bar_label = self.top_bar_label();
self.dropdown.update(ctx, |menu, _ctx| {
menu.set_width_match_position_id(match_width.then_some(top_bar_label));
});
ctx.notify();
}
pub fn add_items(&mut self, items: Vec<DropdownItem<A>>, ctx: &mut ViewContext<Self>) {
self.dropdown.update(ctx, |dropdown, ctx| {
dropdown.add_items(items.iter().map(|item| item.into()));
@@ -294,11 +442,14 @@ where
ctx.notify();
}
// Most dropdowns don't need to use rich menu features like separators, indents, and submenus.
// But some do and, for those, we expose a "rich" item API.
/// Set items from rich menu items.
///
/// Rich menu items already carry erased [`DropdownAction`]s. The dropdown dispatches selected
/// item actions through normal action propagation, so callers should ensure each action is
/// handled by an appropriate view in the containing view hierarchy.
pub fn set_rich_items(
&mut self,
items: impl IntoIterator<Item = MenuItem<DropdownAction<A>>>,
items: impl IntoIterator<Item = MenuItem<DropdownAction>>,
ctx: &mut ViewContext<Self>,
) {
self.dropdown.update(ctx, |dropdown, ctx| {
@@ -345,12 +496,10 @@ where
/// this clears the selection.
///
/// This is primarily useful when items are dynamically generated and correspond to some backing data that's captured by the action.
pub fn set_selected_by_action(&mut self, action: A, ctx: &mut ViewContext<Self>)
where
A: PartialEq,
{
pub fn set_selected_by_action(&mut self, action: A, ctx: &mut ViewContext<Self>) {
let action = DropdownAction::SelectActionAndClose(Box::new(action));
self.dropdown.update(ctx, |dropdown, ctx| {
dropdown.set_selected_by_action(&DropdownAction::SelectActionAndClose(action), ctx);
dropdown.set_selected_by_action(&action, ctx);
ctx.notify();
});
self.selected_item = self.selected_item(ctx);
@@ -380,7 +529,7 @@ where
})
}
fn selected_item(&self, ctx: &mut ViewContext<Self>) -> Option<MenuItem<DropdownAction<A>>> {
fn selected_item(&self, ctx: &mut ViewContext<Self>) -> Option<MenuItem<DropdownAction>> {
self.dropdown
.read(ctx, |dropdown, _| dropdown.selected_item())
}
@@ -390,7 +539,11 @@ where
ctx.notify();
}
fn select_action_and_close(&mut self, action: &A, ctx: &mut ViewContext<Self>) {
fn select_action_and_close(
&mut self,
action: &dyn DropdownItemAction,
ctx: &mut ViewContext<Self>,
) {
ctx.dispatch_typed_action(action);
self.close(ctx);
}
@@ -404,6 +557,11 @@ where
pub fn toggle_expanded(&mut self, ctx: &mut ViewContext<Self>) {
self.is_expanded = !self.is_expanded;
if self.is_expanded {
if self.match_menu_width_to_top_bar {
if let Some(bounds) = ctx.element_position_by_id(self.top_bar_label()) {
self.set_menu_width(bounds.width(), ctx);
}
}
ctx.focus(&self.dropdown);
ctx.emit(DropdownEvent::ToggleExpanded);
}
@@ -458,6 +616,10 @@ where
font_color: self.font_color,
font_size: self.font_size,
padding: self.padding,
background: self.background,
border_color: self.border_color,
border_width: self.border_width,
border_radius: self.border_radius,
..Default::default()
})
.set_clicked_styles(None);
@@ -472,7 +634,7 @@ where
}
let top_bar_element = top_bar.build().on_click(|ctx, _, _| {
ctx.dispatch_typed_action(DropdownAction::<A>::ToggleExpanded);
ctx.dispatch_typed_action(DropdownAction::ToggleExpanded);
});
SavePosition::new(
@@ -506,23 +668,23 @@ where
impl<A> Entity for Dropdown<A>
where
A: Action + Clone,
A: DropdownItemAction,
{
type Event = DropdownEvent;
}
impl<A> TypedActionView for Dropdown<A>
where
A: Action + Clone,
A: DropdownItemAction,
{
type Action = DropdownAction<A>;
type Action = DropdownAction;
fn handle_action(&mut self, action: &DropdownAction<A>, ctx: &mut ViewContext<Self>) {
fn handle_action(&mut self, action: &DropdownAction, ctx: &mut ViewContext<Self>) {
match action {
DropdownAction::Focus(delta) => self.focus(*delta, ctx),
DropdownAction::Close => self.close(ctx),
DropdownAction::SelectActionAndClose(action) => {
self.select_action_and_close(action, ctx)
self.select_action_and_close(action.as_ref(), ctx)
}
DropdownAction::ToggleExpanded => self.toggle_expanded(ctx),
}
@@ -531,7 +693,7 @@ where
impl<A> View for Dropdown<A>
where
A: Action + Clone,
A: DropdownItemAction,
{
fn ui_name() -> &'static str {
"Dropdown"
@@ -547,16 +709,18 @@ where
.with_drop_shadow(DropShadow::default())
.finish();
}
dropdown_stack.add_positioned_overlay_child(
menu,
OffsetPositioning::offset_from_save_position_element(
self.top_bar_label(),
vec2f(0., 0.),
PositionedElementOffsetBounds::WindowByPosition,
self.element_anchor,
self.child_anchor,
),
let positioning = OffsetPositioning::offset_from_save_position_element(
self.top_bar_label(),
vec2f(0., 0.),
PositionedElementOffsetBounds::WindowByPosition,
self.element_anchor,
self.child_anchor,
);
if self.use_overlay_layer {
dropdown_stack.add_positioned_overlay_child(menu, positioning);
} else {
dropdown_stack.add_positioned_child(menu, positioning);
}
}
Container::new(dropdown_stack.finish())
.with_margin_top(self.vertical_margin)
+7 -8
View File
@@ -1,13 +1,12 @@
use galaxyui::{
elements::{
ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, Flex, Hoverable,
MouseStateHandle, ParentElement, Radius, Text,
},
platform::Cursor,
AppContext, Element, Entity, SingletonEntity, TypedActionView, View, ViewContext,
use galaxyui::elements::{
ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, Flex, Hoverable, MouseStateHandle,
ParentElement, Radius, Text,
};
use warpui::platform::Cursor;
use warpui::{AppContext, Element, Entity, SingletonEntity, TypedActionView, View, ViewContext};
use crate::{appearance::Appearance, ui_components::icons::Icon};
use crate::appearance::Appearance;
use crate::ui_components::icons::Icon;
pub enum NewFeaturePopupLabel {
/// A static label.
+261 -123
View File
@@ -1,32 +1,32 @@
use super::dropdown::{
DropdownAction, DropdownItem, MenuHeaderTextFormatter, DROPDOWN_PADDING, TOP_MENU_BAR_HEIGHT,
TOP_MENU_BAR_MAX_WIDTH,
};
use crate::{
appearance::Appearance,
editor::{
EditorView, Event as EditorEvent, PropagateAndNoOpNavigationKeys, SingleLineEditorOptions,
TextOptions,
},
menu::{Event as MenuEvent, Menu, MenuItem, MenuVariant},
ui_components::icons,
};
use std::marker::PhantomData;
use galaxy_editor::editor::NavigationKey;
use galaxyui::{
elements::{
Align, Border, ChildAnchor, ChildView, Clipped, ConstrainedBox, Container, CornerRadius,
CrossAxisAlignment, Dismiss, Element, EventHandler, Flex, MainAxisAlignment, MainAxisSize,
MouseStateHandle, OffsetPositioning, ParentElement, PositionedElementAnchor,
PositionedElementOffsetBounds, Radius, SavePosition, Shrinkable, Stack,
},
geometry::vector::vec2f,
ui_components::{
button::{ButtonVariant, TextAndIcon, TextAndIconAlignment},
components::{Coords, UiComponent, UiComponentStyles},
},
Action, AppContext, BlurContext, Entity, FocusContext, SingletonEntity, TypedActionView, View,
ViewContext, ViewHandle,
use galaxyui::elements::{
Align, Border, ChildAnchor, ChildView, Clipped, ConstrainedBox, Container, CornerRadius,
CrossAxisAlignment, Dismiss, DispatchEventResult, Element, EventHandler, Flex,
MainAxisAlignment, MainAxisSize, MouseStateHandle, OffsetPositioning, ParentElement,
PositionedElementAnchor, PositionedElementOffsetBounds, Radius, SavePosition, Shrinkable,
Stack,
};
use warpui::geometry::vector::vec2f;
use warpui::ui_components::button::{ButtonVariant, TextAndIcon, TextAndIconAlignment};
use warpui::ui_components::components::{Coords, UiComponent, UiComponentStyles};
use warpui::{
AppContext, BlurContext, Entity, FocusContext, SingletonEntity, TypedActionView, View,
ViewContext, ViewHandle, WeakViewHandle,
};
use super::dropdown::{
DropdownAction, DropdownItem, DropdownItemAction, MenuHeaderTextFormatter, DROPDOWN_PADDING,
TOP_MENU_BAR_HEIGHT, TOP_MENU_BAR_MAX_WIDTH,
};
use crate::appearance::Appearance;
use crate::editor::{
EditorView, Event as EditorEvent, PropagateAndNoOpNavigationKeys, SingleLineEditorOptions,
TextOptions,
};
use crate::menu::{Event as MenuEvent, Menu, MenuItem, MenuVariant};
use crate::ui_components::icons;
const EMPTY_DROPDOWN_HEIGHT: f32 = 50.0;
@@ -42,22 +42,29 @@ pub enum FilterableDropdownOrientation {
Down,
}
pub struct FilterableDropdown<A: Action + Clone> {
pub struct FilterableDropdown<A: DropdownItemAction = ()> {
is_expanded: bool,
disabled: bool,
top_bar_mouse_state: MouseStateHandle,
top_bar_max_width: f32,
main_axis_size: MainAxisSize,
dropdown: ViewHandle<Menu<DropdownAction<A>>>,
dropdown: ViewHandle<Menu<DropdownAction>>,
filter_editor: ViewHandle<EditorView>,
selected_item: Option<MenuItem<DropdownAction<A>>>,
items: Vec<DropdownItem<A>>,
self_handle: WeakViewHandle<Self>,
selected_item: Option<MenuItem<DropdownAction>>,
items: Vec<MenuItem<DropdownAction>>,
orientation: FilterableDropdownOrientation,
static_menu_header: Option<&'static str>,
button_variant: ButtonVariant,
style_override: Option<UiComponentStyles>,
hovered_style_override: Option<UiComponentStyles>,
menu_header_text_override: Option<MenuHeaderTextFormatter>,
/// Optional placeholder shown in the closed top bar when no item is
/// selected. Setting a placeholder also opts the dropdown into allowing an
/// empty selection: `set_filtered_items` will not auto-highlight the first
/// item when nothing has been selected yet, so the top bar stays blank
/// instead of leaking the first item as a phantom selection.
placeholder: Option<String>,
/// True when a pinned footer has been registered via `set_footer`.
/// When true, the footer lives inside the `Menu`'s own `Dismiss` (via
/// `Menu::set_pinned_footer_builder`), so clicks on it never trigger the
@@ -68,11 +75,18 @@ pub struct FilterableDropdown<A: Action + Clone> {
menu_width: Option<f32>,
vertical_margin: f32,
top_bar_height: f32,
/// See `Dropdown::use_overlay_layer`. Mirrors the same opt-out for
/// `FilterableDropdown` callers (the orchestrate environment
/// picker) that need to render in the parent's Normal layer
/// instead of an overlay.
use_overlay_layer: bool,
match_menu_width_to_top_bar: bool,
_action_type: PhantomData<A>,
}
impl<A> FilterableDropdown<A>
where
A: Action + Clone,
A: DropdownItemAction,
{
pub fn new(ctx: &mut ViewContext<Self>) -> Self {
let theme = Appearance::as_ref(ctx).theme();
@@ -110,6 +124,7 @@ where
disabled: false,
dropdown,
filter_editor,
self_handle: ctx.handle(),
top_bar_mouse_state: Default::default(),
top_bar_max_width: TOP_MENU_BAR_MAX_WIDTH,
main_axis_size: MainAxisSize::Max,
@@ -121,13 +136,40 @@ where
style_override: None,
hovered_style_override: None,
menu_header_text_override: None,
placeholder: None,
has_pinned_footer: false,
menu_width: None,
vertical_margin: DROPDOWN_PADDING,
top_bar_height: TOP_MENU_BAR_HEIGHT,
use_overlay_layer: true,
match_menu_width_to_top_bar: false,
_action_type: PhantomData,
}
}
/// See `Dropdown::set_use_overlay_layer`.
pub fn set_use_overlay_layer(&mut self, use_overlay_layer: bool, ctx: &mut ViewContext<Self>) {
self.use_overlay_layer = use_overlay_layer;
ctx.notify();
}
/// Override the top-bar height.
/// so callers (e.g. the orchestrate environment picker) that mix
/// `Dropdown` and `FilterableDropdown` in the same row can size them
/// identically.
pub fn set_top_bar_height(&mut self, height: f32, ctx: &mut ViewContext<Self>) {
self.top_bar_height = height;
ctx.notify();
}
/// Override the vertical margin applied above and below the dropdown's top
/// bar (default [`DROPDOWN_PADDING`]). Set to `0.` when the caller manages
/// its own spacing and needs the bar to align flush with sibling inputs.
pub fn set_vertical_margin(&mut self, vertical_margin: f32, ctx: &mut ViewContext<Self>) {
self.vertical_margin = vertical_margin;
ctx.notify();
}
pub fn set_menu_header_text_override<F>(&mut self, formatter: F)
where
F: Fn(&str) -> String + 'static,
@@ -135,6 +177,15 @@ where
self.menu_header_text_override = Some(Box::new(formatter));
}
/// Sets placeholder text shown (greyed) in the closed top bar when no item
/// is selected, and opts the dropdown into allowing an empty selection so
/// the placeholder is preserved rather than being replaced by the first
/// item after filtering.
pub fn set_placeholder(&mut self, placeholder: impl Into<String>, ctx: &mut ViewContext<Self>) {
self.placeholder = Some(placeholder.into());
ctx.notify();
}
pub fn set_footer<F>(&mut self, builder: F, ctx: &mut ViewContext<Self>)
where
F: Fn(&AppContext) -> Box<dyn Element> + 'static,
@@ -181,12 +232,12 @@ where
}
pub fn add_items(&mut self, items: Vec<DropdownItem<A>>, ctx: &mut ViewContext<Self>) {
self.items.extend(items.iter().cloned());
self.items.extend(items.iter().map(|item| item.into()));
self.set_filtered_items(ctx);
}
pub fn set_items(&mut self, items: Vec<DropdownItem<A>>, ctx: &mut ViewContext<Self>) {
self.items = items;
self.items = items.iter().map(|item| item.into()).collect();
self.set_filtered_items(ctx);
// set_filtered_items intentionally preserves self.selected_item when
@@ -196,43 +247,31 @@ where
// items is stale and must be cleared — otherwise the top bar shows a
// ghost label and selected_item_label() returns a value that doesn't
// correspond to any actual item.
let label = self.current_selected_item_label();
if !label.is_empty() && !self.items.iter().any(|item| item.display_text == label) {
let label = self.current_selected_item_label().to_string();
if !label.is_empty()
&& !self
.items
.iter()
.any(|item| Self::item_label(item) == Some(label.as_str()))
{
self.selected_item = None;
ctx.notify();
}
}
/// Set items from rich menu items (MenuItem). This passes the rich menu items to the
/// internal dropdown but also extracts searchable DropdownItem objects for filtering.
/// Set items from rich menu items (MenuItem). This preserves the rich menu items for
/// filtering and passes the filtered items to the internal dropdown.
///
/// Rich menu items already carry erased [`DropdownAction`]s. The dropdown dispatches selected
/// item actions through normal action propagation, so callers should ensure each action is
/// handled by an appropriate view in the containing view hierarchy.
pub fn set_rich_items(
&mut self,
items: Vec<MenuItem<DropdownAction<A>>>,
items: Vec<MenuItem<DropdownAction>>,
ctx: &mut ViewContext<Self>,
) {
// Extract simple DropdownItem objects from MenuItem for filtering
self.items = items
.iter()
.filter_map(|item| match item {
MenuItem::Item(fields) => {
let label = fields.label().to_string();
fields.on_select_action().and_then(|action| {
if let DropdownAction::SelectActionAndClose(a) = action {
Some(DropdownItem::new(label, a.clone()))
} else {
None
}
})
}
_ => None, // Skip headers and separators
})
.collect();
// Set the full rich items on the internal dropdown
self.dropdown.update(ctx, |dropdown, ctx| {
dropdown.set_items(items, ctx);
});
ctx.notify();
self.items = items;
self.set_filtered_items(ctx);
}
/// The number of items in the dropdown.
@@ -285,12 +324,10 @@ where
/// this clears the selection.
///
/// This is primarily useful when items are dynamically generated and correspond to some backing data that's captured by the action.
pub fn set_selected_by_action(&mut self, action: A, ctx: &mut ViewContext<Self>)
where
A: PartialEq,
{
pub fn set_selected_by_action(&mut self, action: A, ctx: &mut ViewContext<Self>) {
let action = DropdownAction::SelectActionAndClose(Box::new(action));
self.dropdown.update(ctx, |dropdown, ctx| {
dropdown.set_selected_by_action(&DropdownAction::SelectActionAndClose(action), ctx);
dropdown.set_selected_by_action(&action, ctx);
});
self.selected_item = self.selected_item_in_dropdown(ctx);
ctx.notify();
@@ -308,6 +345,22 @@ where
})
}
/// When enabled, the open menu sizes itself to the last rendered width of
/// the dropdown's top bar. This is useful for flexible dropdowns whose
/// trigger width is determined by parent layout rather than a fixed max.
pub fn set_match_menu_width_to_top_bar(
&mut self,
match_width: bool,
ctx: &mut ViewContext<Self>,
) {
self.match_menu_width_to_top_bar = match_width;
let top_bar_label = self.top_bar_label();
self.dropdown.update(ctx, |menu, _ctx| {
menu.set_width_match_position_id(match_width.then_some(top_bar_label));
});
ctx.notify();
}
pub fn set_disabled(&mut self, ctx: &mut ViewContext<Self>) {
self.disabled = true;
ctx.notify();
@@ -321,7 +374,7 @@ where
fn selected_item_in_dropdown(
&self,
ctx: &mut ViewContext<Self>,
) -> Option<MenuItem<DropdownAction<A>>> {
) -> Option<MenuItem<DropdownAction>> {
self.dropdown
.read(ctx, |dropdown, _| dropdown.selected_item())
}
@@ -342,8 +395,10 @@ where
}
fn focus(&mut self, _delta: usize, ctx: &mut ViewContext<Self>) {
ctx.focus(&self.filter_editor);
ctx.notify();
if self.is_expanded {
ctx.focus(&self.filter_editor);
ctx.notify();
}
}
/// Dispatches the item's action up the responder chain and then closes the
@@ -356,7 +411,11 @@ where
/// dropdown from their `Select`-equivalent branch, or `update_view` will
/// panic with "Circular view update". The dropdown is already closed here
/// after the dispatch returns, so parents don't need to close it themselves.
fn select_action_and_close(&mut self, action: &A, ctx: &mut ViewContext<Self>) {
fn select_action_and_close(
&mut self,
action: &dyn DropdownItemAction,
ctx: &mut ViewContext<Self>,
) {
// Check against the length of the dropdown to no-op in the case
// there aren't any elements being rendered
if self.dropdown_items_len(ctx) > 0 {
@@ -376,6 +435,11 @@ where
pub(crate) fn toggle_expanded(&mut self, ctx: &mut ViewContext<Self>) {
self.is_expanded = !self.is_expanded;
if self.is_expanded {
if self.match_menu_width_to_top_bar {
if let Some(bounds) = ctx.element_position_by_id(self.top_bar_label()) {
self.set_menu_width(bounds.width(), ctx);
}
}
ctx.focus(&self.filter_editor);
ctx.emit(FilterableDropdownEvent::ToggleExpanded);
}
@@ -387,8 +451,8 @@ where
}
fn render_closed_top_bar(&self, appearance: &Appearance) -> Box<dyn Element> {
let (selected_item_text, font_family_id) = match self.static_menu_header {
Some(header) => (header.to_string(), None),
let (selected_item_text, font_family_id, is_placeholder) = match self.static_menu_header {
Some(header) => (header.to_string(), None, false),
None => match self.selected_item.clone() {
Some(MenuItem::Item(fields)) => {
let label = fields.label();
@@ -397,12 +461,33 @@ where
} else {
label.to_string()
};
(text, fields.override_font_family())
(text, fields.override_font_family(), false)
}
_ => (String::new(), None),
_ => match &self.placeholder {
Some(placeholder) => (placeholder.clone(), None, true),
None => (String::new(), None, false),
},
},
};
let mut base_style = self.style_override.unwrap_or(UiComponentStyles {
padding: Some(Coords {
top: 5.,
bottom: 5.,
left: 8.,
right: 8.,
}),
..Default::default()
});
if is_placeholder {
base_style.font_color = Some(
appearance
.theme()
.sub_text_color(appearance.theme().surface_1())
.into(),
);
}
let mut top_bar = appearance
.ui_builder()
.button(self.button_variant, self.top_bar_mouse_state.clone())
@@ -418,15 +503,7 @@ where
)
.with_inner_padding(10.),
)
.with_style(self.style_override.unwrap_or(UiComponentStyles {
padding: Some(Coords {
top: 5.,
bottom: 5.,
left: 8.,
right: 8.,
}),
..Default::default()
}))
.with_style(base_style)
.set_clicked_styles(None);
if let Some(hovered_style) = self.hovered_style_override {
@@ -441,11 +518,30 @@ where
top_bar =
top_bar.with_style(UiComponentStyles::default().set_font_family_id(font_family_id))
}
top_bar
let disabled = self.disabled;
let self_handle = self.self_handle.clone();
let dropdown = self.dropdown.clone();
let filter_editor = self.filter_editor.clone();
let top_bar_element = top_bar
.build()
.on_click(|ctx, _, _| {
ctx.dispatch_typed_action(DropdownAction::<A>::ToggleExpanded);
ctx.dispatch_typed_action(DropdownAction::ToggleExpanded);
})
.finish();
EventHandler::new(top_bar_element)
.on_keydown(move |ctx, app, keystroke| {
let is_focused = self_handle
.upgrade(app)
.is_some_and(|handle| handle.is_focused(app))
|| dropdown.is_focused(app)
|| filter_editor.is_focused(app);
if !disabled && is_focused && keystroke.is_unmodified_key(" ") {
ctx.dispatch_typed_action(DropdownAction::ToggleExpanded);
DispatchEventResult::StopPropagation
} else {
DispatchEventResult::PropagateToParent
}
})
.finish()
}
@@ -533,14 +629,14 @@ where
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(4.)))
.finish(),
)
.with_max_width(self.top_bar_max_width)
.with_max_width(self.menu_width.unwrap_or(self.top_bar_max_width))
.with_height(EMPTY_DROPDOWN_HEIGHT)
.finish();
// Wrap with Dismiss to handle clicks outside the empty menu
Dismiss::new(EventHandler::new(empty_menu).finish())
.on_dismiss(|ctx, _app| {
ctx.dispatch_typed_action(DropdownAction::<A>::Close);
ctx.dispatch_typed_action(DropdownAction::Close);
})
.prevent_interaction_with_other_elements()
.finish()
@@ -615,34 +711,57 @@ where
// it won't be visible in the newly computed list of filtered items.
// If it isn't, we set the selected element to the first index of
// the new elements such that there's always a candidate element to select.
let current_label = self.current_selected_item_label();
let current_label = self.current_selected_item_label().to_string();
// When a placeholder is configured and nothing is selected yet, don't
// auto-highlight the first item: doing so emits `ItemSelected`, which
// the subscription would cache as a phantom selection and surface in
// the top bar (defeating the blank/placeholder state).
let allow_empty_selection = self.placeholder.is_some() && current_label.is_empty();
let mut current_label_not_visible = true;
self.dropdown.update(ctx, |dropdown, ctx| {
dropdown.set_items(
self.items
.iter()
.filter(|item| {
let item_matches_filter =
item.display_text.to_lowercase().contains(&filter_query);
if item.display_text == current_label && item_matches_filter {
let item_matches_filter = Self::item_matches_filter(item, &filter_query);
if Self::item_label(item) == Some(current_label.as_str())
&& item_matches_filter
{
current_label_not_visible = false;
};
item_matches_filter
})
.map(|item| item.into()),
.cloned(),
ctx,
);
if current_label_not_visible && !dropdown.is_empty() {
dropdown.set_selected_by_index(0, ctx);
if allow_empty_selection {
dropdown.reset_selection(ctx);
} else {
dropdown.set_selected_by_index(0, ctx);
}
} else {
dropdown.set_selected_by_name(current_label, ctx);
dropdown.set_selected_by_name(&current_label, ctx);
}
ctx.notify();
});
ctx.notify();
}
fn item_label(item: &MenuItem<DropdownAction>) -> Option<&str> {
match item {
MenuItem::Item(fields) => Some(fields.label()),
_ => None,
}
}
fn item_matches_filter(item: &MenuItem<DropdownAction>, filter_query: &str) -> bool {
filter_query.is_empty()
|| Self::item_label(item)
.is_some_and(|label| label.to_lowercase().contains(filter_query))
}
fn dropdown_items_len(&self, ctx: &AppContext) -> usize {
self.dropdown.as_ref(ctx).items_len()
}
@@ -657,27 +776,44 @@ where
pub fn set_menu_header_to_static(&mut self, header: &'static str) {
self.static_menu_header = Some(header);
}
/// Test-only: drive the filter input with `query` and re-filter the list,
/// mirroring what happens when a user types into the search field.
#[cfg(test)]
pub(crate) fn set_filter_query_for_test(&mut self, query: &str, ctx: &mut ViewContext<Self>) {
self.filter_editor.update(ctx, |editor, ctx| {
editor.select_all(ctx);
editor.insert_selected_text(query, ctx);
});
self.set_filtered_items(ctx);
}
/// Test-only: the number of items currently visible after filtering.
#[cfg(test)]
pub(crate) fn visible_items_len_for_test(&self, ctx: &AppContext) -> usize {
self.dropdown_items_len(ctx)
}
}
impl<A> Entity for FilterableDropdown<A>
where
A: Action + Clone,
A: DropdownItemAction,
{
type Event = FilterableDropdownEvent;
}
impl<A> TypedActionView for FilterableDropdown<A>
where
A: Action + Clone,
A: DropdownItemAction,
{
type Action = DropdownAction<A>;
type Action = DropdownAction;
fn handle_action(&mut self, action: &DropdownAction<A>, ctx: &mut ViewContext<Self>) {
fn handle_action(&mut self, action: &DropdownAction, ctx: &mut ViewContext<Self>) {
match action {
DropdownAction::Focus(delta) => self.focus(*delta, ctx),
DropdownAction::Close => self.close(ctx),
DropdownAction::SelectActionAndClose(action) => {
self.select_action_and_close(action, ctx)
self.select_action_and_close(action.as_ref(), ctx)
}
DropdownAction::ToggleExpanded => self.toggle_expanded(ctx),
}
@@ -686,14 +822,14 @@ where
impl<A> View for FilterableDropdown<A>
where
A: Action + Clone,
A: DropdownItemAction,
{
fn ui_name() -> &'static str {
"FilterableDropdown"
}
fn on_focus(&mut self, focus_ctx: &FocusContext, ctx: &mut ViewContext<Self>) {
if focus_ctx.is_self_focused() {
if focus_ctx.is_self_focused() && self.is_expanded {
self.focus(0, ctx)
}
}
@@ -712,26 +848,28 @@ where
let mut dropdown_stack = Stack::new().with_child(self.render_top_bar(appearance));
if self.is_expanded {
dropdown_stack.add_positioned_overlay_child(
dropdown_menu,
if self.orientation == FilterableDropdownOrientation::Down {
OffsetPositioning::offset_from_save_position_element(
self.top_bar_label(),
vec2f(0., 0.),
PositionedElementOffsetBounds::WindowByPosition,
PositionedElementAnchor::BottomLeft,
ChildAnchor::TopLeft,
)
} else {
OffsetPositioning::offset_from_save_position_element(
self.top_bar_label(),
vec2f(0., 0.),
PositionedElementOffsetBounds::WindowByPosition,
PositionedElementAnchor::TopLeft,
ChildAnchor::BottomLeft,
)
},
);
let positioning = if self.orientation == FilterableDropdownOrientation::Down {
OffsetPositioning::offset_from_save_position_element(
self.top_bar_label(),
vec2f(0., 0.),
PositionedElementOffsetBounds::WindowByPosition,
PositionedElementAnchor::BottomLeft,
ChildAnchor::TopLeft,
)
} else {
OffsetPositioning::offset_from_save_position_element(
self.top_bar_label(),
vec2f(0., 0.),
PositionedElementOffsetBounds::WindowByPosition,
PositionedElementAnchor::TopLeft,
ChildAnchor::BottomLeft,
)
};
if self.use_overlay_layer {
dropdown_stack.add_positioned_overlay_child(dropdown_menu, positioning);
} else {
dropdown_stack.add_positioned_child(dropdown_menu, positioning);
}
}
Container::new(dropdown_stack.finish())
.with_margin_top(self.vertical_margin)
+47 -25
View File
@@ -1,3 +1,22 @@
use pathfinder_color::ColorU;
use serde::Serialize;
pub use warpui::accessibility::{AccessibilityContent, WarpA11yRole};
use warpui::elements::{
Align, Border, ChildAnchor, Clipped, ConstrainedBox, Container, CornerRadius,
CrossAxisAlignment, DropShadow, Element, Flex, Hoverable, MouseStateHandle, OffsetPositioning,
ParentAnchor, ParentOffsetBounds, Radius, SavePosition, Shrinkable, Text,
};
pub use warpui::elements::{ParentElement as _, Stack};
pub use warpui::geometry::vector::vec2f;
use warpui::keymap::EditableBinding;
use warpui::presenter::ChildView;
use warpui::ui_components::components::UiComponent;
pub use warpui::AppContext;
use warpui::{
Entity, FocusContext, ModelHandle, SingletonEntity, TypedActionView, View, ViewContext,
ViewHandle,
};
use crate::appearance::Appearance;
use crate::editor::{
EditorView, Event as EditorEvent, PropagateAndNoOpNavigationKeys, SingleLineEditorOptions,
@@ -6,30 +25,9 @@ use crate::editor::{
use crate::send_telemetry_from_ctx;
use crate::server::telemetry::{FindOption, TelemetryEvent};
use crate::settings::InputModeSettings;
use crate::ui_components::{blended_colors, icons::Icon};
use serde::Serialize;
use crate::themes::theme::Fill;
use galaxyui::elements::{ChildAnchor, OffsetPositioning, Radius, SavePosition, Shrinkable};
use galaxyui::keymap::EditableBinding;
use galaxyui::ui_components::components::UiComponent;
pub use galaxyui::{
accessibility::{AccessibilityContent, GalaxyA11yRole},
elements::{ParentElement as _, Stack},
geometry::vector::vec2f,
AppContext,
};
use galaxyui::{
elements::{
Align, Border, Clipped, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment,
DropShadow, Element, Flex, Hoverable, MouseStateHandle, ParentAnchor, ParentOffsetBounds,
Text,
},
Entity, SingletonEntity, TypedActionView, View,
};
use galaxyui::{presenter::ChildView, ViewContext, ViewHandle};
use galaxyui::{FocusContext, ModelHandle};
use pathfinder_color::ColorU;
use crate::ui_components::blended_colors;
use crate::ui_components::icons::Icon;
pub const FIND_BAR_WIDTH: f32 = 500.;
const ICON_PADDING: f32 = 4.;
@@ -69,6 +67,12 @@ pub trait FindModel {
FindDirection::Down => FindDirection::Up,
}
}
/// Returns true if a find operation is currently in progress (scanning).
/// Default implementation returns false for synchronous find implementations.
fn is_scanning(&self) -> bool {
false
}
}
pub enum Event {
@@ -317,13 +321,31 @@ impl<T: FindModel + Entity<Event = FindEvent> + 'static> Find<T> {
}
fn render_match_index(&self, appearance: &Appearance, app: &AppContext) -> Box<dyn Element> {
let model = self.model.as_ref(app);
// Show scanning indicator if async find is in progress.
if model.is_scanning() {
let match_count = model.match_count();
let label = if match_count > 0 {
format!("{}+ ...", match_count)
} else {
"Scanning...".to_string()
};
return Text::new_inline(label, appearance.ui_font_family(), FIND_EDITOR_FONT_SIZE)
.with_color(blended_colors::text_sub(
appearance.theme(),
appearance.theme().surface_1(),
))
.finish();
}
// If there is some match index, we add 1 to it since the UI is 1-indexed
// (i.e. first match starts at index 1 out of the total number of matches).
let index = match self.model.as_ref(app).focused_match_index() {
let index = match model.focused_match_index() {
None => 0,
Some(idx) => idx + 1,
};
let label = format!("{}/{}", index, self.model.as_ref(app).match_count());
let label = format!("{}/{}", index, model.match_count());
Text::new_inline(label, appearance.ui_font_family(), FIND_EDITOR_FONT_SIZE)
.with_color(blended_colors::text_sub(
appearance.theme(),
+6 -3
View File
@@ -2,8 +2,13 @@ use galaxy_core::ui::appearance::Appearance;
use galaxyui::{platform::WindowStyle, App};
use std::sync::Arc;
use warpui::platform::WindowStyle;
use warpui::App;
use super::{Find, FindDirection, FindEvent, FindModel};
use crate::auth::AuthStateProvider;
use crate::server::server_api::{team::MockTeamClient, workspace::MockWorkspaceClient};
use crate::server::server_api::team::MockTeamClient;
use crate::server::server_api::workspace::MockWorkspaceClient;
use crate::server::telemetry::context_provider::AppTelemetryContextProvider;
use crate::settings_view::keybindings::KeybindingChangedNotifier;
use crate::test_util::settings::initialize_settings_for_tests;
@@ -11,8 +16,6 @@ use crate::vim_registers::VimRegisters;
use crate::workspace::sync_inputs::SyncedInputState;
use crate::workspaces::user_workspaces::UserWorkspaces;
use super::{Find, FindDirection, FindEvent, FindModel};
struct MockFindModel;
impl galaxyui::Entity for MockFindModel {
@@ -1,5 +1,3 @@
use crate::appearance::Appearance;
use crate::notebooks::file::MarkdownDisplayMode;
use galaxyui::elements::{CornerRadius, Fill as UiFill, Radius};
use galaxyui::presenter::ChildView;
use galaxyui::ui_components::components::UiComponentStyles;
@@ -10,6 +8,9 @@ use galaxyui::{
AppContext, Element, Entity, SingletonEntity, TypedActionView, View, ViewContext, ViewHandle,
};
use crate::appearance::Appearance;
use crate::notebooks::file::MarkdownDisplayMode;
#[derive(Debug, Clone)]
pub enum MarkdownToggleEvent {
ModeSelected(MarkdownDisplayMode),
+4 -2
View File
@@ -24,9 +24,11 @@ pub use clickable_text_input::*;
pub use compact_dropdown::{CompactDropdown, CompactDropdownEvent, CompactDropdownItem};
pub use copyable_text_field::*;
pub use dismissible_toast::*;
pub use dropdown::{Dropdown, DropdownEvent, DropdownItem};
pub use dropdown::{Dropdown, DropdownAction, DropdownEvent, DropdownItem, DropdownItemAction};
pub use feature_popup::*;
pub use filterable_dropdown::{FilterableDropdown, FilterableDropdownOrientation};
pub use filterable_dropdown::{
FilterableDropdown, FilterableDropdownEvent, FilterableDropdownOrientation,
};
pub use markdown_toggle_view::{MarkdownToggleEvent, MarkdownToggleView};
pub use submittable_text_input::*;
pub use warning_box::*;
@@ -1,19 +1,19 @@
use pathfinder_color::ColorU;
use galaxyui::elements::{
Border, Container, CornerRadius, CrossAxisAlignment, Flex, MainAxisAlignment, MainAxisSize,
MouseStateHandle, ParentElement, Radius, Shrinkable,
};
use galaxyui::ui_components::components::{Coords, UiComponent, UiComponentStyles};
use galaxyui::{
elements::{
Border, Container, CornerRadius, CrossAxisAlignment, Flex, MainAxisAlignment, MainAxisSize,
MouseStateHandle, ParentElement, Radius, Shrinkable,
},
ui_components::components::{Coords, UiComponent, UiComponentStyles},
AppContext, Element, Entity, FocusContext, SingletonEntity, TypedActionView, View, ViewContext,
ViewHandle,
};
use pathfinder_color::ColorU;
use crate::{
appearance::Appearance,
editor::{EditorOptions, EditorView, Event as EditorEvent, InteractionState, TextOptions},
themes::theme::Fill,
use crate::appearance::Appearance;
use crate::editor::{
EditorOptions, EditorView, Event as EditorEvent, InteractionState, TextOptions,
};
use crate::themes::theme::Fill;
const ENTER_BUTTON_SIZE: f32 = 22.;
+44 -15
View File
@@ -1,10 +1,11 @@
//! A reusable warning callout component with optional action button.
use markdown_parser::{FormattedText, FormattedTextInline, FormattedTextLine};
use galaxy_core::ui::color::blend::Blend;
use galaxyui::color::ColorU;
use galaxyui::elements::{
Border, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, Element, Expanded, Flex,
Hoverable, MainAxisSize, MouseStateHandle, ParentElement, Radius, Text,
FormattedTextElement, Hoverable, HyperlinkLens, MainAxisSize, MouseStateHandle, ParentElement,
Radius, Text,
};
use galaxyui::platform::Cursor;
use galaxyui::EventContext;
@@ -32,10 +33,14 @@ impl WarningBoxButtonConfig {
}
}
}
pub enum WarningBoxTitle {
Text(String),
Formatted(FormattedTextInline),
}
pub struct WarningBoxConfig {
pub icon: Icon,
pub title: String,
pub title: WarningBoxTitle,
pub description: Option<String>,
/// Optional max width. If provided, the WarningBox will not exceed this width,
@@ -49,9 +54,17 @@ pub struct WarningBoxConfig {
impl WarningBoxConfig {
pub fn new(title: impl Into<String>) -> Self {
Self::from_title(WarningBoxTitle::Text(title.into()))
}
pub fn formatted_title(title: FormattedTextInline) -> Self {
Self::from_title(WarningBoxTitle::Formatted(title))
}
fn from_title(title: WarningBoxTitle) -> Self {
Self {
icon: Icon::AlertTriangle,
title: title.into(),
title,
description: None,
width: None,
margin_top: 8.,
@@ -79,7 +92,6 @@ impl WarningBoxConfig {
self
}
}
pub fn render_warning_box(config: WarningBoxConfig, appearance: &Appearance) -> Box<dyn Element> {
let theme = appearance.theme();
let icon_size = appearance.ui_font_size() * 1.1;
@@ -96,19 +108,36 @@ pub fn render_warning_box(config: WarningBoxConfig, appearance: &Appearance) ->
let background = theme.surface_2().blend(&warning_fill.with_opacity(15));
let title = match config.title {
WarningBoxTitle::Text(title) => Text::new(
title,
appearance.ui_font_family(),
appearance.ui_font_size(),
)
.with_color(text_color)
.soft_wrap(true)
.finish(),
WarningBoxTitle::Formatted(title) => FormattedTextElement::new(
FormattedText::new([FormattedTextLine::Line(title)]),
appearance.ui_font_size(),
appearance.ui_font_family(),
appearance.ui_font_family(),
text_color,
Default::default(),
)
.with_hyperlink_font_color(theme.accent().into())
.register_default_click_handlers_with_action_support(|hyperlink, _event, app| {
if let HyperlinkLens::Url(url) = hyperlink {
app.open_url(url);
}
})
.finish(),
};
let mut text_col = Flex::column()
.with_cross_axis_alignment(CrossAxisAlignment::Start)
.with_spacing(2.)
.with_child(
Text::new(
config.title,
appearance.ui_font_family(),
appearance.ui_font_size(),
)
.with_color(text_color)
.soft_wrap(true)
.finish(),
);
.with_child(title);
if let Some(description) = config.description {
text_col.add_child(