first pass of merging in warp (doesn't build)
This commit is contained in:
@@ -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>>,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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(¤t_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)
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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.;
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user