menu: Add scrollable support to menu. (#310)
<img width="274" alt="image" src="https://github.com/user-attachments/assets/eaff16a6-1294-4381-96c0-c178c195999b">
This commit is contained in:
parent
cf88a48bec
commit
cbed7b1ea1
9 changed files with 268 additions and 136 deletions
|
|
@ -1,8 +1,8 @@
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, div, impl_actions, px, AnchorCorner, AppContext, DismissEvent, Element, EventEmitter,
|
actions, div, impl_actions, px, AnchorCorner, AppContext, DismissEvent, Element, EventEmitter,
|
||||||
FocusHandle, FocusableView, InteractiveElement, IntoElement, KeyBinding, MouseButton,
|
FocusHandle, FocusableView, InteractiveElement, IntoElement, KeyBinding, MouseButton,
|
||||||
MouseDownEvent, ParentElement as _, Render, Styled as _, View, ViewContext, VisualContext,
|
MouseDownEvent, ParentElement as _, Render, SharedString, Styled as _, View, ViewContext,
|
||||||
WindowContext,
|
VisualContext, WindowContext,
|
||||||
};
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use ui::{
|
use ui::{
|
||||||
|
|
@ -289,6 +289,20 @@ impl Render for PopupStory {
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
.child(
|
||||||
|
Button::new("popup-menu-11112")
|
||||||
|
.label("Scrollable Menu")
|
||||||
|
.popup_menu(move |this, _| {
|
||||||
|
let mut this = this.scrollable();
|
||||||
|
for i in 0..100 {
|
||||||
|
this = this.menu(
|
||||||
|
SharedString::from(format!("Item {}", i)),
|
||||||
|
Box::new(Info(i)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
this
|
||||||
|
}),
|
||||||
|
)
|
||||||
.child(self.message.clone()),
|
.child(self.message.clone()),
|
||||||
)
|
)
|
||||||
.child("Right click to open ContextMenu")
|
.child("Right click to open ContextMenu")
|
||||||
|
|
|
||||||
|
|
@ -273,6 +273,10 @@ impl Disableable for Button {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Selectable for Button {
|
impl Selectable for Button {
|
||||||
|
fn element_id(&self) -> &ElementId {
|
||||||
|
&self.id
|
||||||
|
}
|
||||||
|
|
||||||
fn selected(mut self, selected: bool) -> Self {
|
fn selected(mut self, selected: bool) -> Self {
|
||||||
self.selected = selected;
|
self.selected = selected;
|
||||||
self
|
self
|
||||||
|
|
@ -305,6 +309,12 @@ impl ParentElement for Button {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl InteractiveElement for Button {
|
||||||
|
fn interactivity(&mut self) -> &mut gpui::Interactivity {
|
||||||
|
self.base.interactivity()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl RenderOnce for Button {
|
impl RenderOnce for Button {
|
||||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||||
let style: ButtonStyle = self.style;
|
let style: ButtonStyle = self.style;
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,10 @@ impl Disableable for Checkbox {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Selectable for Checkbox {
|
impl Selectable for Checkbox {
|
||||||
|
fn element_id(&self) -> &ElementId {
|
||||||
|
&self.id
|
||||||
|
}
|
||||||
|
|
||||||
fn selected(self, selected: bool) -> Self {
|
fn selected(self, selected: bool) -> Self {
|
||||||
self.checked(selected)
|
self.checked(selected)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ use crate::{h_flex, theme::ActiveTheme, Disableable, Icon, IconName, Selectable,
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct ListItem {
|
pub struct ListItem {
|
||||||
|
id: ElementId,
|
||||||
base: Stateful<Div>,
|
base: Stateful<Div>,
|
||||||
disabled: bool,
|
disabled: bool,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
|
|
@ -23,8 +24,10 @@ pub struct ListItem {
|
||||||
|
|
||||||
impl ListItem {
|
impl ListItem {
|
||||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||||
|
let id: ElementId = id.into();
|
||||||
Self {
|
Self {
|
||||||
base: h_flex().id(id.into()).gap_x_1().py_1().px_2().text_base(),
|
id: id.clone(),
|
||||||
|
base: h_flex().id(id).gap_x_1().py_1().px_2().text_base(),
|
||||||
disabled: false,
|
disabled: false,
|
||||||
selected: false,
|
selected: false,
|
||||||
confirmed: false,
|
confirmed: false,
|
||||||
|
|
@ -98,6 +101,10 @@ impl Disableable for ListItem {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Selectable for ListItem {
|
impl Selectable for ListItem {
|
||||||
|
fn element_id(&self) -> &ElementId {
|
||||||
|
&self.id
|
||||||
|
}
|
||||||
|
|
||||||
fn selected(mut self, selected: bool) -> Self {
|
fn selected(mut self, selected: bool) -> Self {
|
||||||
self.selected = selected;
|
self.selected = selected;
|
||||||
self
|
self
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ use gpui::{
|
||||||
AppContext, Bounds, DismissEvent, DispatchPhase, Element, ElementId, EventEmitter, FocusHandle,
|
AppContext, Bounds, DismissEvent, DispatchPhase, Element, ElementId, EventEmitter, FocusHandle,
|
||||||
FocusableView, GlobalElementId, Hitbox, InteractiveElement as _, IntoElement, KeyBinding,
|
FocusableView, GlobalElementId, Hitbox, InteractiveElement as _, IntoElement, KeyBinding,
|
||||||
LayoutId, ManagedView, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, Render,
|
LayoutId, ManagedView, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, Render,
|
||||||
Style, Styled, View, ViewContext, VisualContext, WindowContext,
|
SharedString, Style, Styled, View, ViewContext, VisualContext, WindowContext,
|
||||||
};
|
};
|
||||||
use std::{cell::RefCell, rc::Rc};
|
use std::{cell::RefCell, rc::Rc};
|
||||||
|
|
||||||
|
|
@ -131,7 +131,7 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_trigger(&mut self, is_open: bool, cx: &mut WindowContext) -> impl IntoElement {
|
fn render_trigger(&mut self, is_open: bool, cx: &mut WindowContext) -> impl IntoElement {
|
||||||
let base = div().id("popover-trigger");
|
let base = div().id(SharedString::from(format!("{}-trigger", self.id)));
|
||||||
|
|
||||||
if self.trigger.is_none() {
|
if self.trigger.is_none() {
|
||||||
return base;
|
return base;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
use std::cell::Cell;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
|
@ -6,8 +7,12 @@ use gpui::{
|
||||||
FocusHandle, InteractiveElement, IntoElement, KeyBinding, ParentElement, Pixels, Render,
|
FocusHandle, InteractiveElement, IntoElement, KeyBinding, ParentElement, Pixels, Render,
|
||||||
SharedString, Styled as _, View, ViewContext, VisualContext as _, WindowContext,
|
SharedString, Styled as _, View, ViewContext, VisualContext as _, WindowContext,
|
||||||
};
|
};
|
||||||
use gpui::{anchored, canvas, rems, AnchorCorner, Bounds, FocusableView, Keystroke, WeakView};
|
use gpui::{
|
||||||
|
anchored, canvas, rems, AnchorCorner, Bounds, Edges, FocusableView, Keystroke, ScrollHandle,
|
||||||
|
StatefulInteractiveElement, WeakView,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::scroll::{Scrollbar, ScrollbarState};
|
||||||
use crate::StyledExt;
|
use crate::StyledExt;
|
||||||
use crate::{
|
use crate::{
|
||||||
button::Button, h_flex, list::ListItem, popover::Popover, theme::ActiveTheme, v_flex, Icon,
|
button::Button, h_flex, list::ListItem, popover::Popover, theme::ActiveTheme, v_flex, Icon,
|
||||||
|
|
@ -31,7 +36,8 @@ pub trait PopupMenuExt: Selectable + IntoElement + 'static {
|
||||||
self,
|
self,
|
||||||
f: impl Fn(PopupMenu, &mut ViewContext<PopupMenu>) -> PopupMenu + 'static,
|
f: impl Fn(PopupMenu, &mut ViewContext<PopupMenu>) -> PopupMenu + 'static,
|
||||||
) -> Popover<PopupMenu> {
|
) -> Popover<PopupMenu> {
|
||||||
Popover::new("popup-menu")
|
let element_id = self.element_id();
|
||||||
|
Popover::new(SharedString::from(format!("popup-menu:{:?}", element_id)))
|
||||||
.no_style()
|
.no_style()
|
||||||
.trigger(self)
|
.trigger(self)
|
||||||
.content(move |cx| PopupMenu::build(cx, |menu, cx| f(menu, cx)))
|
.content(move |cx| PopupMenu::build(cx, |menu, cx| f(menu, cx)))
|
||||||
|
|
@ -80,6 +86,10 @@ pub struct PopupMenu {
|
||||||
hovered_menu_ix: Option<usize>,
|
hovered_menu_ix: Option<usize>,
|
||||||
bounds: Bounds<Pixels>,
|
bounds: Bounds<Pixels>,
|
||||||
|
|
||||||
|
scrollable: bool,
|
||||||
|
scroll_handle: ScrollHandle,
|
||||||
|
scroll_state: Rc<Cell<ScrollbarState>>,
|
||||||
|
|
||||||
action_focus_handle: Option<FocusHandle>,
|
action_focus_handle: Option<FocusHandle>,
|
||||||
_subscriptions: [gpui::Subscription; 1],
|
_subscriptions: [gpui::Subscription; 1],
|
||||||
}
|
}
|
||||||
|
|
@ -106,6 +116,9 @@ impl PopupMenu {
|
||||||
has_icon: false,
|
has_icon: false,
|
||||||
hovered_menu_ix: None,
|
hovered_menu_ix: None,
|
||||||
bounds: Bounds::default(),
|
bounds: Bounds::default(),
|
||||||
|
scrollable: false,
|
||||||
|
scroll_handle: ScrollHandle::default(),
|
||||||
|
scroll_state: Rc::new(Cell::new(ScrollbarState::default())),
|
||||||
_subscriptions: [_on_blur_subscription],
|
_subscriptions: [_on_blur_subscription],
|
||||||
};
|
};
|
||||||
cx.refresh();
|
cx.refresh();
|
||||||
|
|
@ -131,6 +144,14 @@ impl PopupMenu {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the menu to be scrollable to show vertical scrollbar.
|
||||||
|
///
|
||||||
|
/// NOTE: If this is true, the sub-menus will cannot be support.
|
||||||
|
pub fn scrollable(mut self) -> Self {
|
||||||
|
self.scrollable = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Add Menu Item
|
/// Add Menu Item
|
||||||
pub fn menu(mut self, label: impl Into<SharedString>, action: Box<dyn Action>) -> Self {
|
pub fn menu(mut self, label: impl Into<SharedString>, action: Box<dyn Action>) -> Self {
|
||||||
self.add_menu_item(label, None, action);
|
self.add_menu_item(label, None, action);
|
||||||
|
|
@ -414,20 +435,24 @@ impl PopupMenu {
|
||||||
impl FluentBuilder for PopupMenu {}
|
impl FluentBuilder for PopupMenu {}
|
||||||
impl EventEmitter<DismissEvent> for PopupMenu {}
|
impl EventEmitter<DismissEvent> for PopupMenu {}
|
||||||
impl FocusableView for PopupMenu {
|
impl FocusableView for PopupMenu {
|
||||||
fn focus_handle(&self, _: &gpui::AppContext) -> FocusHandle {
|
fn focus_handle(&self, _: &AppContext) -> FocusHandle {
|
||||||
self.focus_handle.clone()
|
self.focus_handle.clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for PopupMenu {
|
impl Render for PopupMenu {
|
||||||
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl gpui::IntoElement {
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
let view = cx.view().clone();
|
let view = cx.view().clone();
|
||||||
let has_icon = self.menu_items.iter().any(|item| item.has_icon());
|
let has_icon = self.menu_items.iter().any(|item| item.has_icon());
|
||||||
let items_count = self.menu_items.len();
|
let items_count = self.menu_items.len();
|
||||||
let max_width = self.max_width;
|
let max_width = self.max_width;
|
||||||
let bounds = self.bounds;
|
let bounds = self.bounds;
|
||||||
|
|
||||||
|
let window_haft_height = cx.window_bounds().get_bounds().size.height * 0.5;
|
||||||
|
let max_height = window_haft_height.min(px(450.));
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
|
.id("popup-menu")
|
||||||
.key_context("PopupMenu")
|
.key_context("PopupMenu")
|
||||||
.track_focus(&self.focus_handle)
|
.track_focus(&self.focus_handle)
|
||||||
.on_action(cx.listener(Self::select_next))
|
.on_action(cx.listener(Self::select_next))
|
||||||
|
|
@ -435,141 +460,203 @@ impl Render for PopupMenu {
|
||||||
.on_action(cx.listener(Self::confirm))
|
.on_action(cx.listener(Self::confirm))
|
||||||
.on_action(cx.listener(Self::dismiss))
|
.on_action(cx.listener(Self::dismiss))
|
||||||
.on_mouse_down_out(cx.listener(|this, _, cx| this.dismiss(&Dismiss, cx)))
|
.on_mouse_down_out(cx.listener(|this, _, cx| this.dismiss(&Dismiss, cx)))
|
||||||
.max_h(self.max_width)
|
|
||||||
.min_w(self.min_width)
|
|
||||||
.p_1()
|
|
||||||
.gap_y_0p5()
|
|
||||||
.min_w(rems(8.))
|
|
||||||
.popover_style(cx)
|
.popover_style(cx)
|
||||||
.text_color(cx.theme().popover_foreground)
|
.text_color(cx.theme().popover_foreground)
|
||||||
.relative()
|
.relative()
|
||||||
.child({
|
.child(
|
||||||
canvas(
|
div()
|
||||||
move |bounds, cx| view.update(cx, |r, _| r.bounds = bounds),
|
.id("popup-menu-items")
|
||||||
|_, _, _| {},
|
.when(self.scrollable, |this| {
|
||||||
)
|
this.max_h(max_height)
|
||||||
.absolute()
|
.overflow_y_scroll()
|
||||||
.size_full()
|
.track_scroll(&self.scroll_handle)
|
||||||
})
|
})
|
||||||
.children(
|
.child(
|
||||||
self.menu_items
|
v_flex()
|
||||||
.iter_mut()
|
.p_1()
|
||||||
.enumerate()
|
.gap_y_0p5()
|
||||||
// Skip last separator
|
.min_w(self.min_width)
|
||||||
.filter(|(ix, item)| !(*ix == items_count - 1 && item.is_separator()))
|
.max_w(self.max_width)
|
||||||
.map(|(ix, item)| {
|
.min_w(rems(8.))
|
||||||
let group_id = format!("item:{}", ix);
|
.child({
|
||||||
|
canvas(
|
||||||
|
move |bounds, cx| view.update(cx, |r, _| r.bounds = bounds),
|
||||||
|
|_, _, _| {},
|
||||||
|
)
|
||||||
|
.absolute()
|
||||||
|
.size_full()
|
||||||
|
})
|
||||||
|
.children(
|
||||||
|
self.menu_items
|
||||||
|
.iter_mut()
|
||||||
|
.enumerate()
|
||||||
|
// Skip last separator
|
||||||
|
.filter(|(ix, item)| {
|
||||||
|
!(*ix == items_count - 1 && item.is_separator())
|
||||||
|
})
|
||||||
|
.map(|(ix, item)| {
|
||||||
|
let group_id = format!("item:{}", ix);
|
||||||
|
|
||||||
let this = ListItem::new(("menu-item", ix))
|
let this = ListItem::new(("menu-item", ix))
|
||||||
.group(group_id.clone())
|
.group(group_id.clone())
|
||||||
.relative()
|
.relative()
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.py_0()
|
.py_0()
|
||||||
.px_2()
|
.px_2()
|
||||||
.h(px(28.))
|
.rounded_md()
|
||||||
.rounded_md()
|
|
||||||
.items_center()
|
|
||||||
.on_mouse_enter(cx.listener(move |this, _, cx| {
|
|
||||||
this.hovered_menu_ix = Some(ix);
|
|
||||||
cx.notify();
|
|
||||||
}));
|
|
||||||
|
|
||||||
match item {
|
|
||||||
PopupMenuItem::Separator => this.h_auto().p_0().disabled(true).child(
|
|
||||||
div()
|
|
||||||
.rounded_none()
|
|
||||||
.h(px(1.))
|
|
||||||
.mx_neg_1()
|
|
||||||
.my_0p5()
|
|
||||||
.bg(cx.theme().muted),
|
|
||||||
),
|
|
||||||
PopupMenuItem::Item {
|
|
||||||
icon,
|
|
||||||
label,
|
|
||||||
action,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
let action = action.as_ref().map(|action| action.boxed_clone());
|
|
||||||
let key = Self::render_keybinding(action, cx);
|
|
||||||
|
|
||||||
this.on_click(cx.listener(move |this, _, cx| this.on_click(ix, cx)))
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.items_center()
|
.items_center()
|
||||||
.gap_x_1p5()
|
.on_mouse_enter(cx.listener(move |this, _, cx| {
|
||||||
.children(Self::render_icon(has_icon, icon.clone(), cx))
|
this.hovered_menu_ix = Some(ix);
|
||||||
.child(
|
cx.notify();
|
||||||
h_flex()
|
}));
|
||||||
.flex_1()
|
|
||||||
.gap_2()
|
match item {
|
||||||
.items_center()
|
PopupMenuItem::Separator => {
|
||||||
.justify_between()
|
this.h_auto().p_0().disabled(true).child(
|
||||||
.child(label.clone())
|
div()
|
||||||
.children(key),
|
.rounded_none()
|
||||||
),
|
.h(px(1.))
|
||||||
)
|
.mx_neg_1()
|
||||||
}
|
.my_0p5()
|
||||||
PopupMenuItem::Submenu { icon, label, menu } => this
|
.bg(cx.theme().muted),
|
||||||
.when(self.hovered_menu_ix == Some(ix), |this| this.selected(true))
|
)
|
||||||
.child(
|
}
|
||||||
h_flex()
|
PopupMenuItem::Item {
|
||||||
.items_start()
|
icon,
|
||||||
.child(
|
label,
|
||||||
h_flex()
|
action,
|
||||||
.size_full()
|
..
|
||||||
.items_center()
|
} => {
|
||||||
.gap_x_1p5()
|
let action = action
|
||||||
.children(Self::render_icon(
|
.as_ref()
|
||||||
has_icon,
|
.map(|action| action.boxed_clone());
|
||||||
icon.clone(),
|
let key = Self::render_keybinding(action, cx);
|
||||||
cx,
|
|
||||||
))
|
this.on_click(cx.listener(move |this, _, cx| {
|
||||||
|
this.on_click(ix, cx)
|
||||||
|
}))
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.flex_1()
|
.h(px(26.))
|
||||||
.gap_2()
|
|
||||||
.items_center()
|
.items_center()
|
||||||
.justify_between()
|
.gap_x_1p5()
|
||||||
.child(label.clone())
|
.children(Self::render_icon(
|
||||||
.child(IconName::ChevronRight),
|
has_icon,
|
||||||
),
|
icon.clone(),
|
||||||
)
|
cx,
|
||||||
.when_some(self.hovered_menu_ix, |this, hovered_ix| {
|
))
|
||||||
let (anchor, left) = if cx.bounds().size.width
|
.child(
|
||||||
- bounds.origin.x
|
h_flex()
|
||||||
< max_width
|
.flex_1()
|
||||||
{
|
.gap_2()
|
||||||
(AnchorCorner::TopRight, -px(15.))
|
.items_center()
|
||||||
} else {
|
.justify_between()
|
||||||
(AnchorCorner::TopLeft, bounds.size.width - px(10.))
|
.child(label.clone())
|
||||||
};
|
.children(key),
|
||||||
|
),
|
||||||
let top = if bounds.origin.y + bounds.size.height
|
|
||||||
> cx.bounds().size.height
|
|
||||||
{
|
|
||||||
px(32.)
|
|
||||||
} else {
|
|
||||||
-px(10.)
|
|
||||||
};
|
|
||||||
|
|
||||||
if hovered_ix == ix {
|
|
||||||
this.child(
|
|
||||||
anchored().anchor(anchor).child(
|
|
||||||
div()
|
|
||||||
.occlude()
|
|
||||||
.top(top)
|
|
||||||
.left(left)
|
|
||||||
.child(menu.clone()),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
this
|
|
||||||
}
|
}
|
||||||
}),
|
PopupMenuItem::Submenu { icon, label, menu } => this
|
||||||
),
|
.when(self.hovered_menu_ix == Some(ix), |this| {
|
||||||
}
|
this.selected(true)
|
||||||
}),
|
})
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.items_start()
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.size_full()
|
||||||
|
.items_center()
|
||||||
|
.gap_x_1p5()
|
||||||
|
.children(Self::render_icon(
|
||||||
|
has_icon,
|
||||||
|
icon.clone(),
|
||||||
|
cx,
|
||||||
|
))
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.flex_1()
|
||||||
|
.gap_2()
|
||||||
|
.items_center()
|
||||||
|
.justify_between()
|
||||||
|
.child(label.clone())
|
||||||
|
.child(
|
||||||
|
IconName::ChevronRight,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.when_some(
|
||||||
|
self.hovered_menu_ix,
|
||||||
|
|this, hovered_ix| {
|
||||||
|
let (anchor, left) =
|
||||||
|
if cx.bounds().size.width
|
||||||
|
- bounds.origin.x
|
||||||
|
< max_width
|
||||||
|
{
|
||||||
|
(
|
||||||
|
AnchorCorner::TopRight,
|
||||||
|
-px(15.),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
AnchorCorner::TopLeft,
|
||||||
|
bounds.size.width
|
||||||
|
- px(10.),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let top = if bounds.origin.y
|
||||||
|
+ bounds.size.height
|
||||||
|
> cx.bounds().size.height
|
||||||
|
{
|
||||||
|
px(32.)
|
||||||
|
} else {
|
||||||
|
-px(10.)
|
||||||
|
};
|
||||||
|
|
||||||
|
if hovered_ix == ix {
|
||||||
|
this.child(
|
||||||
|
anchored()
|
||||||
|
.anchor(anchor)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.occlude()
|
||||||
|
.top(top)
|
||||||
|
.left(left)
|
||||||
|
.child(menu.clone()),
|
||||||
|
)
|
||||||
|
.snap_to_window_with_margin(
|
||||||
|
Edges::all(px(8.)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
this
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
.when(self.scrollable, |this| {
|
||||||
|
// TODO: When the menu is limited by `overflow_y_scroll`, the sub-menu will cannot be displayed.
|
||||||
|
this.child(
|
||||||
|
div()
|
||||||
|
.absolute()
|
||||||
|
.top_1()
|
||||||
|
.left_1()
|
||||||
|
.right_1()
|
||||||
|
.bottom_1()
|
||||||
|
.child(Scrollbar::vertical(
|
||||||
|
cx.entity_id(),
|
||||||
|
self.scroll_state.clone(),
|
||||||
|
self.scroll_handle.clone(),
|
||||||
|
self.bounds.size,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ use gpui::{
|
||||||
};
|
};
|
||||||
|
|
||||||
const MIN_THUMB_SIZE: f32 = 80.;
|
const MIN_THUMB_SIZE: f32 = 80.;
|
||||||
const THUMB_RADIUS: Pixels = Pixels(5.0);
|
const THUMB_RADIUS: Pixels = Pixels(4.0);
|
||||||
const THUMB_INSET: Pixels = Pixels(2.);
|
const THUMB_INSET: Pixels = Pixels(2.);
|
||||||
|
|
||||||
pub trait ScrollHandleOffsetable {
|
pub trait ScrollHandleOffsetable {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,9 @@ use crate::{
|
||||||
scroll::{Scrollable, ScrollbarAxis},
|
scroll::{Scrollable, ScrollbarAxis},
|
||||||
theme::ActiveTheme,
|
theme::ActiveTheme,
|
||||||
};
|
};
|
||||||
use gpui::{div, px, Axis, Div, Element, EntityId, FocusHandle, Pixels, Styled, WindowContext};
|
use gpui::{
|
||||||
|
div, px, Axis, Div, Element, ElementId, EntityId, FocusHandle, Pixels, Styled, WindowContext,
|
||||||
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
/// Returns a `Div` as horizontal flex layout.
|
/// Returns a `Div` as horizontal flex layout.
|
||||||
|
|
@ -152,6 +154,7 @@ impl From<Pixels> for Size {
|
||||||
|
|
||||||
/// A trait for defining element that can be selected.
|
/// A trait for defining element that can be selected.
|
||||||
pub trait Selectable: Sized {
|
pub trait Selectable: Sized {
|
||||||
|
fn element_id(&self) -> &ElementId;
|
||||||
/// Set the selected state of the element.
|
/// Set the selected state of the element.
|
||||||
fn selected(self, selected: bool) -> Self;
|
fn selected(self, selected: bool) -> Self;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ use gpui::{
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct Tab {
|
pub struct Tab {
|
||||||
|
id: ElementId,
|
||||||
base: Stateful<Div>,
|
base: Stateful<Div>,
|
||||||
label: AnyElement,
|
label: AnyElement,
|
||||||
prefix: Option<AnyElement>,
|
prefix: Option<AnyElement>,
|
||||||
|
|
@ -18,8 +19,10 @@ pub struct Tab {
|
||||||
|
|
||||||
impl Tab {
|
impl Tab {
|
||||||
pub fn new(id: impl Into<ElementId>, label: impl IntoElement) -> Self {
|
pub fn new(id: impl Into<ElementId>, label: impl IntoElement) -> Self {
|
||||||
|
let id: ElementId = id.into();
|
||||||
Self {
|
Self {
|
||||||
base: div().id(id.into()).gap_1().py_1p5().px_3().h(px(30.)),
|
id: id.clone(),
|
||||||
|
base: div().id(id).gap_1().py_1p5().px_3().h(px(30.)),
|
||||||
label: label.into_any_element(),
|
label: label.into_any_element(),
|
||||||
disabled: false,
|
disabled: false,
|
||||||
selected: false,
|
selected: false,
|
||||||
|
|
@ -42,6 +45,10 @@ impl Tab {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Selectable for Tab {
|
impl Selectable for Tab {
|
||||||
|
fn element_id(&self) -> &ElementId {
|
||||||
|
&self.id
|
||||||
|
}
|
||||||
|
|
||||||
fn selected(mut self, selected: bool) -> Self {
|
fn selected(mut self, selected: bool) -> Self {
|
||||||
self.selected = selected;
|
self.selected = selected;
|
||||||
self
|
self
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue