chore: migrate to gpui-base #51
Generated
+586
-417
File diff suppressed because it is too large
Load Diff
+15
-8
@@ -9,14 +9,21 @@ edition = "2024"
|
|||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
# GPUI
|
# GPUI. The `gpui-pre` family is upstream zed's gpui republished unchanged, so these
|
||||||
gpui = { git = "https://github.com/zed-industries/zed" }
|
# aliases keep every `use gpui::..` site as it is while moving off the zed git pin.
|
||||||
gpui_platform = { git = "https://github.com/zed-industries/zed", features = ["font-kit", "x11", "wayland"] }
|
gpui = { package = "gpui-pre", version = "0.3.5" }
|
||||||
gpui_linux = { git = "https://github.com/zed-industries/zed" }
|
gpui_platform = { package = "gpui-pre-platform", version = "0.3.5", features = ["font-kit", "x11", "wayland"] }
|
||||||
gpui_windows = { git = "https://github.com/zed-industries/zed" }
|
gpui_linux = { package = "gpui-pre-linux", version = "0.3.5" }
|
||||||
gpui_macos = { git = "https://github.com/zed-industries/zed" }
|
gpui_windows = { package = "gpui-pre-windows", version = "0.3.5" }
|
||||||
gpui_tokio = { git = "https://github.com/zed-industries/zed" }
|
gpui_macos = { package = "gpui-pre-macos", version = "0.3.5" }
|
||||||
reqwest_client = { git = "https://github.com/zed-industries/zed" }
|
gpui_web = { package = "gpui-pre-web", version = "0.3.5" }
|
||||||
|
gpui_util = { package = "gpui-pre-util", version = "0.3.5" }
|
||||||
|
sum_tree = { package = "gpui-pre-sum-tree", version = "0.3.5" }
|
||||||
|
reqwest_client = { package = "gpui-pre-reqwest-client", version = "0.3.5" }
|
||||||
|
gpui_tokio = { path = "crates/gpui_tokio" }
|
||||||
|
|
||||||
|
# Unstyled behavior, state, and infrastructure from GPUI Kit
|
||||||
|
gpui-base = "0.6.1"
|
||||||
|
|
||||||
# Nostr
|
# Nostr
|
||||||
nostr-lmdb = { git = "https://github.com/rust-nostr/nostr" }
|
nostr-lmdb = { git = "https://github.com/rust-nostr/nostr" }
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ use theme::ActiveTheme;
|
|||||||
use ui::avatar::Avatar;
|
use ui::avatar::Avatar;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::dock::{Panel, PanelEvent};
|
use ui::dock::{Panel, PanelEvent};
|
||||||
use ui::input::{Input, InputEvent, InputState};
|
use ui::input::{Input, InputEvent, InputState, Textarea, TextareaState};
|
||||||
use ui::menu::DropdownMenu;
|
use ui::menu::DropdownMenu;
|
||||||
use ui::notification::Notification;
|
use ui::notification::Notification;
|
||||||
use ui::scroll::Scrollbar;
|
use ui::scroll::Scrollbar;
|
||||||
@@ -85,7 +85,7 @@ pub struct ChatPanel {
|
|||||||
reports_by_id: Arc<RwLock<BTreeMap<EventId, Vec<SendReport>>>>,
|
reports_by_id: Arc<RwLock<BTreeMap<EventId, Vec<SendReport>>>>,
|
||||||
|
|
||||||
/// Chat input state
|
/// Chat input state
|
||||||
input: Entity<InputState>,
|
input: Entity<TextareaState>,
|
||||||
|
|
||||||
/// Subject input state
|
/// Subject input state
|
||||||
subject_input: Entity<InputState>,
|
subject_input: Entity<InputState>,
|
||||||
@@ -142,7 +142,7 @@ impl ChatPanel {
|
|||||||
|
|
||||||
// Define input state
|
// Define input state
|
||||||
let input = cx.new(|cx| {
|
let input = cx.new(|cx| {
|
||||||
InputState::new(window, cx)
|
TextareaState::new(window, cx)
|
||||||
.placeholder(format!("Message {}", name))
|
.placeholder(format!("Message {}", name))
|
||||||
.auto_grow(1, 20)
|
.auto_grow(1, 20)
|
||||||
.clean_on_escape()
|
.clean_on_escape()
|
||||||
@@ -2108,7 +2108,7 @@ impl Render for ChatPanel {
|
|||||||
this.upload(window, cx);
|
this.upload(window, cx);
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
.child(Input::new(&self.input).appearance(false).flex_1())
|
.child(Textarea::new(&self.input).appearance(false).flex_1())
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.pl_1()
|
.pl_1()
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[package]
|
||||||
|
name = "gpui_tokio"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow.workspace = true
|
||||||
|
gpui.workspace = true
|
||||||
|
gpui_util.workspace = true
|
||||||
|
tokio = { version = "1", features = ["rt", "rt-multi-thread"] }
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
//! Vendored from zed's `crates/gpui_tokio` (Apache-2.0) because the `gpui-pre` family
|
||||||
|
//! does not republish it, and `nostr-sdk`'s reqwest client needs a Tokio runtime.
|
||||||
|
|
||||||
|
use std::future::Future;
|
||||||
|
|
||||||
|
use gpui::{App, AppContext, Global, ReadGlobal, Task};
|
||||||
|
use gpui_util::defer;
|
||||||
|
pub use tokio::task::JoinError;
|
||||||
|
|
||||||
|
/// Initializes the Tokio wrapper using a new Tokio runtime with 2 worker threads.
|
||||||
|
///
|
||||||
|
/// If you need more threads (or access to the runtime outside of GPUI), you can create the runtime
|
||||||
|
/// yourself and pass a Handle to `init_from_handle`.
|
||||||
|
pub fn init(cx: &mut App) {
|
||||||
|
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||||||
|
// Since we now have two executors, let's try to keep our footprint small
|
||||||
|
.worker_threads(2)
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.expect("Failed to initialize Tokio");
|
||||||
|
|
||||||
|
let handle = runtime.handle().clone();
|
||||||
|
cx.set_global(GlobalTokio {
|
||||||
|
owned_runtime: Some(runtime),
|
||||||
|
handle,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initializes the Tokio wrapper using a Tokio runtime handle.
|
||||||
|
pub fn init_from_handle(cx: &mut App, handle: tokio::runtime::Handle) {
|
||||||
|
cx.set_global(GlobalTokio {
|
||||||
|
owned_runtime: None,
|
||||||
|
handle,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
struct GlobalTokio {
|
||||||
|
owned_runtime: Option<tokio::runtime::Runtime>,
|
||||||
|
handle: tokio::runtime::Handle,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Global for GlobalTokio {}
|
||||||
|
|
||||||
|
impl Drop for GlobalTokio {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if let Some(runtime) = self.owned_runtime.take() {
|
||||||
|
runtime.shutdown_background();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Tokio {}
|
||||||
|
|
||||||
|
impl Tokio {
|
||||||
|
/// Spawns the given future on Tokio's thread pool, and returns it via a GPUI task
|
||||||
|
/// Note that the Tokio task will be cancelled if the GPUI task is dropped
|
||||||
|
pub fn spawn<C, Fut, R>(cx: &C, f: Fut) -> Task<Result<R, JoinError>>
|
||||||
|
where
|
||||||
|
C: AppContext,
|
||||||
|
Fut: Future<Output = R> + Send + 'static,
|
||||||
|
R: Send + 'static,
|
||||||
|
{
|
||||||
|
cx.read_global(|tokio: &GlobalTokio, cx| {
|
||||||
|
let join_handle = tokio.handle.spawn(f);
|
||||||
|
let abort_handle = join_handle.abort_handle();
|
||||||
|
let cancel = defer(move || {
|
||||||
|
abort_handle.abort();
|
||||||
|
});
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
let result = join_handle.await;
|
||||||
|
drop(cancel);
|
||||||
|
result
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spawns the given future on Tokio's thread pool, and returns it via a GPUI task
|
||||||
|
/// Note that the Tokio task will be cancelled if the GPUI task is dropped
|
||||||
|
pub fn spawn_result<C, Fut, R>(cx: &C, f: Fut) -> Task<anyhow::Result<R>>
|
||||||
|
where
|
||||||
|
C: AppContext,
|
||||||
|
Fut: Future<Output = anyhow::Result<R>> + Send + 'static,
|
||||||
|
R: Send + 'static,
|
||||||
|
{
|
||||||
|
cx.read_global(|tokio: &GlobalTokio, cx| {
|
||||||
|
let join_handle = tokio.handle.spawn(f);
|
||||||
|
let abort_handle = join_handle.abort_handle();
|
||||||
|
let cancel = defer(move || {
|
||||||
|
abort_handle.abort();
|
||||||
|
});
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
let result = join_handle.await?;
|
||||||
|
drop(cancel);
|
||||||
|
result
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle(cx: &App) -> tokio::runtime::Handle {
|
||||||
|
GlobalTokio::global(cx).handle.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ publish.workspace = true
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
|
gpui-base.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
|||||||
@@ -46,6 +46,63 @@ pub fn init(cx: &mut App) {
|
|||||||
Theme::sync_scrollbar_appearance(cx);
|
Theme::sync_scrollbar_appearance(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Mirror the active coop theme into the `gpui-base` global theme.
|
||||||
|
///
|
||||||
|
/// Base paints a few things from its own tokens -- the focus ring, the wash
|
||||||
|
/// behind selected text, scrollbars, and overlay backdrops -- so the two
|
||||||
|
/// globals have to agree or those details drift away from the palette.
|
||||||
|
///
|
||||||
|
/// Only roles base can act on are projected. Radius, spacing, typography sizes,
|
||||||
|
/// shadows, and scrollbar geometry keep their base defaults: coop has a single
|
||||||
|
/// `radius`/`radius_lg`/`font_size` where base has six-point scales, so any
|
||||||
|
/// mapping would be invented rather than derived.
|
||||||
|
///
|
||||||
|
/// This is a no-op before the coop theme global exists; [`Theme::change`] is the
|
||||||
|
/// authoritative hook that keeps the projection current.
|
||||||
|
pub fn sync_base(cx: &mut App) {
|
||||||
|
let Some(theme) = cx.try_global::<Theme>() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let appearance = if theme.mode.is_dark() {
|
||||||
|
gpui_base::ThemeAppearance::Dark
|
||||||
|
} else {
|
||||||
|
gpui_base::ThemeAppearance::Light
|
||||||
|
};
|
||||||
|
let scrollbar_mode = match theme.scrollbar_mode {
|
||||||
|
ScrollbarMode::Scrolling => gpui_base::ScrollbarMode::Scrolling,
|
||||||
|
ScrollbarMode::Hover => gpui_base::ScrollbarMode::Hover,
|
||||||
|
ScrollbarMode::Always => gpui_base::ScrollbarMode::Always,
|
||||||
|
};
|
||||||
|
let colors = theme.colors;
|
||||||
|
let font_family = theme.font_family.clone();
|
||||||
|
|
||||||
|
let base = gpui_base::Theme::global_mut(cx);
|
||||||
|
base.appearance = appearance;
|
||||||
|
base.scrollbar = base.scrollbar.clone().with_mode(scrollbar_mode);
|
||||||
|
base.tokens.typography.sans = font_family;
|
||||||
|
|
||||||
|
let tokens = &mut base.tokens.colors;
|
||||||
|
tokens.background = colors.background;
|
||||||
|
tokens.foreground = colors.text;
|
||||||
|
tokens.surface = colors.surface_background;
|
||||||
|
tokens.surface_foreground = colors.text;
|
||||||
|
tokens.primary = colors.element_background;
|
||||||
|
tokens.primary_foreground = colors.element_foreground;
|
||||||
|
tokens.secondary = colors.secondary_background;
|
||||||
|
tokens.secondary_foreground = colors.secondary_foreground;
|
||||||
|
tokens.muted = colors.ghost_element_background_alt;
|
||||||
|
tokens.muted_foreground = colors.text_muted;
|
||||||
|
tokens.accent = colors.ghost_element_hover;
|
||||||
|
tokens.accent_foreground = colors.text;
|
||||||
|
tokens.destructive = colors.danger_background;
|
||||||
|
tokens.destructive_foreground = colors.danger_foreground;
|
||||||
|
tokens.border = colors.border;
|
||||||
|
tokens.input = colors.border;
|
||||||
|
tokens.ring = colors.ring;
|
||||||
|
tokens.selection = colors.selection;
|
||||||
|
}
|
||||||
|
|
||||||
pub trait ActiveTheme {
|
pub trait ActiveTheme {
|
||||||
fn theme(&self) -> &Theme;
|
fn theme(&self) -> &Theme;
|
||||||
}
|
}
|
||||||
@@ -183,6 +240,9 @@ impl Theme {
|
|||||||
if let Some(window) = window {
|
if let Some(window) = window {
|
||||||
window.refresh();
|
window.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keep the base-layer projection in step with the coop palette
|
||||||
|
sync_base(cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ common = { path = "../common" }
|
|||||||
theme = { path = "../theme" }
|
theme = { path = "../theme" }
|
||||||
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
|
gpui-base.workspace = true
|
||||||
instant.workspace = true
|
instant.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
smallvec.workspace = true
|
smallvec.workspace = true
|
||||||
@@ -16,13 +17,7 @@ anyhow.workspace = true
|
|||||||
itertools.workspace = true
|
itertools.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
|
|
||||||
unicode-segmentation = "1.12.0"
|
|
||||||
uuid = "1.10"
|
uuid = "1.10"
|
||||||
regex = "1"
|
|
||||||
lsp-types = "0.97.0"
|
|
||||||
ropey = { version = "=2.0.0-beta.1", features = ["metric_lines_lf", "metric_utf16"] }
|
|
||||||
sum_tree = { git = "https://github.com/zed-industries/zed" }
|
|
||||||
|
|
||||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
tree-sitter = "0.26"
|
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
use gpui::{actions, Action};
|
|
||||||
use serde::Deserialize;
|
|
||||||
|
|
||||||
/// Define a custom confirm action
|
|
||||||
#[derive(Clone, Action, PartialEq, Eq, Deserialize)]
|
|
||||||
#[action(namespace = list, no_json)]
|
|
||||||
pub struct Confirm {
|
|
||||||
/// Is confirm with secondary.
|
|
||||||
pub secondary: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
actions!(ui, [Cancel, SelectUp, SelectDown, SelectLeft, SelectRight]);
|
|
||||||
+23
-39
@@ -2,15 +2,16 @@ use std::rc::Rc;
|
|||||||
|
|
||||||
use gpui::prelude::FluentBuilder as _;
|
use gpui::prelude::FluentBuilder as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, ClickEvent, Div, ElementId, Hsla, InteractiveElement, IntoElement,
|
AnyElement, App, ClickEvent, ElementId, Hsla, InteractiveElement, IntoElement, MouseButton,
|
||||||
ParentElement, RenderOnce, SharedString, Stateful, StatefulInteractiveElement as _,
|
ParentElement, RenderOnce, SharedString, StatefulInteractiveElement as _, StyleRefinement,
|
||||||
StyleRefinement, Styled, Window, div, relative,
|
Styled, Window, div, relative,
|
||||||
};
|
};
|
||||||
|
use gpui_base::Button as BaseButton;
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
|
|
||||||
use crate::indicator::Indicator;
|
use crate::indicator::Indicator;
|
||||||
use crate::tooltip::Tooltip;
|
use crate::tooltip::Tooltip;
|
||||||
use crate::{Disableable, Icon, IconName, Selectable, Sizable, Size, StyledExt, h_flex};
|
use crate::{Disableable, Icon, IconName, Selectable, Sizable, Size, h_flex};
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
pub struct ButtonCustomVariant {
|
pub struct ButtonCustomVariant {
|
||||||
@@ -114,9 +115,7 @@ pub trait ButtonVariants: Sized {
|
|||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
pub struct Button {
|
pub struct Button {
|
||||||
id: ElementId,
|
base: BaseButton,
|
||||||
base: Stateful<Div>,
|
|
||||||
style: StyleRefinement,
|
|
||||||
|
|
||||||
icon: Option<Icon>,
|
icon: Option<Icon>,
|
||||||
label: Option<SharedString>,
|
label: Option<SharedString>,
|
||||||
@@ -151,12 +150,8 @@ impl From<Button> for AnyElement {
|
|||||||
|
|
||||||
impl Button {
|
impl Button {
|
||||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||||
let id = id.into();
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
id: id.clone(),
|
base: BaseButton::new(id),
|
||||||
base: div().flex_shrink_0().id(id),
|
|
||||||
style: StyleRefinement::default(),
|
|
||||||
icon: None,
|
icon: None,
|
||||||
label: None,
|
label: None,
|
||||||
variant: ButtonVariant::default(),
|
variant: ButtonVariant::default(),
|
||||||
@@ -301,7 +296,7 @@ impl ButtonVariants for Button {
|
|||||||
|
|
||||||
impl Styled for Button {
|
impl Styled for Button {
|
||||||
fn style(&mut self) -> &mut StyleRefinement {
|
fn style(&mut self) -> &mut StyleRefinement {
|
||||||
&mut self.style
|
self.base.style()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -318,7 +313,7 @@ impl InteractiveElement for Button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl RenderOnce for Button {
|
impl RenderOnce for Button {
|
||||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
let style: ButtonVariant = self.variant;
|
let style: ButtonVariant = self.variant;
|
||||||
let clickable = self.clickable();
|
let clickable = self.clickable();
|
||||||
let hoverable = self.hoverable();
|
let hoverable = self.hoverable();
|
||||||
@@ -329,18 +324,21 @@ impl RenderOnce for Button {
|
|||||||
_ => self.size,
|
_ => self.size,
|
||||||
};
|
};
|
||||||
|
|
||||||
let focus_handle = window
|
|
||||||
.use_keyed_state(self.id.clone(), cx, |_window, cx| cx.focus_handle())
|
|
||||||
.read(cx)
|
|
||||||
.clone();
|
|
||||||
|
|
||||||
self.base
|
self.base
|
||||||
.when(!self.disabled, |this| {
|
|
||||||
this.track_focus(
|
|
||||||
&focus_handle
|
|
||||||
.tab_index(self.tab_index)
|
.tab_index(self.tab_index)
|
||||||
.tab_stop(self.tab_stop),
|
.tab_stop(self.tab_stop)
|
||||||
)
|
.disabled(self.disabled)
|
||||||
|
.when_some(self.on_click.clone(), |this, on_click| {
|
||||||
|
this.on_click(move |event, window, cx| {
|
||||||
|
// Stop handle any click event when disabled.
|
||||||
|
// To avoid handle dropdown menu open when button is disabled.
|
||||||
|
if !clickable {
|
||||||
|
cx.stop_propagation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
on_click(event, window, cx);
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.relative()
|
.relative()
|
||||||
.flex_shrink_0()
|
.flex_shrink_0()
|
||||||
@@ -349,7 +347,6 @@ impl RenderOnce for Button {
|
|||||||
.justify_center()
|
.justify_center()
|
||||||
.cursor_default()
|
.cursor_default()
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.refine_style(&self.style)
|
|
||||||
.map(|this| match self.rounded {
|
.map(|this| match self.rounded {
|
||||||
false => this.rounded(cx.theme().radius),
|
false => this.rounded(cx.theme().radius),
|
||||||
true => this.rounded_full(),
|
true => this.rounded_full(),
|
||||||
@@ -399,8 +396,7 @@ impl RenderOnce for Button {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.refine_style(&self.style)
|
.on_mouse_down(MouseButton::Left, move |_, window, cx| {
|
||||||
.on_mouse_down(gpui::MouseButton::Left, move |_, window, cx| {
|
|
||||||
// Stop handle any click event when disabled.
|
// Stop handle any click event when disabled.
|
||||||
// To avoid handle dropdown menu open when button is disabled.
|
// To avoid handle dropdown menu open when button is disabled.
|
||||||
if self.disabled {
|
if self.disabled {
|
||||||
@@ -410,18 +406,6 @@ impl RenderOnce for Button {
|
|||||||
// Avoid focus on mouse down.
|
// Avoid focus on mouse down.
|
||||||
window.prevent_default();
|
window.prevent_default();
|
||||||
})
|
})
|
||||||
.when_some(self.on_click, |this, on_click| {
|
|
||||||
this.on_click(move |event, window, cx| {
|
|
||||||
// Stop handle any click event when disabled.
|
|
||||||
// To avoid handle dropdown menu open when button is disabled.
|
|
||||||
if !clickable {
|
|
||||||
cx.stop_propagation();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
on_click(event, window, cx);
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.when_some(self.on_hover.filter(|_| hoverable), |this, on_hover| {
|
.when_some(self.on_hover.filter(|_| hoverable), |this, on_hover| {
|
||||||
this.on_hover(move |hovered, window, cx| {
|
this.on_hover(move |hovered, window, cx| {
|
||||||
(on_hover)(hovered, window, cx);
|
(on_hover)(hovered, window, cx);
|
||||||
|
|||||||
@@ -1,312 +0,0 @@
|
|||||||
use std::rc::Rc;
|
|
||||||
use instant::Duration;
|
|
||||||
|
|
||||||
use gpui::prelude::FluentBuilder as _;
|
|
||||||
use gpui::{
|
|
||||||
div, px, relative, rems, svg, Animation, AnimationExt, AnyElement, App, Div, ElementId,
|
|
||||||
InteractiveElement, IntoElement, ParentElement, RenderOnce, SharedString,
|
|
||||||
StatefulInteractiveElement, StyleRefinement, Styled, Window,
|
|
||||||
};
|
|
||||||
use theme::ActiveTheme;
|
|
||||||
|
|
||||||
use crate::icon::IconNamed;
|
|
||||||
use crate::{v_flex, Disableable, IconName, Selectable, Sizable, Size, StyledExt as _};
|
|
||||||
|
|
||||||
/// A Checkbox element.
|
|
||||||
#[allow(clippy::type_complexity)]
|
|
||||||
#[derive(IntoElement)]
|
|
||||||
pub struct Checkbox {
|
|
||||||
id: ElementId,
|
|
||||||
base: Div,
|
|
||||||
style: StyleRefinement,
|
|
||||||
label: Option<SharedString>,
|
|
||||||
children: Vec<AnyElement>,
|
|
||||||
checked: bool,
|
|
||||||
disabled: bool,
|
|
||||||
size: Size,
|
|
||||||
tab_stop: bool,
|
|
||||||
tab_index: isize,
|
|
||||||
on_click: Option<Rc<dyn Fn(&bool, &mut Window, &mut App) + 'static>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Checkbox {
|
|
||||||
/// Create a new Checkbox with the given id.
|
|
||||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
|
||||||
Self {
|
|
||||||
id: id.into(),
|
|
||||||
base: div(),
|
|
||||||
style: StyleRefinement::default(),
|
|
||||||
label: None,
|
|
||||||
children: Vec::new(),
|
|
||||||
checked: false,
|
|
||||||
disabled: false,
|
|
||||||
size: Size::default(),
|
|
||||||
on_click: None,
|
|
||||||
tab_stop: true,
|
|
||||||
tab_index: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the label for the checkbox.
|
|
||||||
pub fn label(mut self, label: impl Into<SharedString>) -> Self {
|
|
||||||
self.label = Some(label.into());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the checked state for the checkbox.
|
|
||||||
pub fn checked(mut self, checked: bool) -> Self {
|
|
||||||
self.checked = checked;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the click handler for the checkbox.
|
|
||||||
///
|
|
||||||
/// The `&bool` parameter indicates the new checked state after the click.
|
|
||||||
pub fn on_click(mut self, handler: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self {
|
|
||||||
self.on_click = Some(Rc::new(handler));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the tab stop for the checkbox, default is true.
|
|
||||||
pub fn tab_stop(mut self, tab_stop: bool) -> Self {
|
|
||||||
self.tab_stop = tab_stop;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the tab index for the checkbox, default is 0.
|
|
||||||
pub fn tab_index(mut self, tab_index: isize) -> Self {
|
|
||||||
self.tab_index = tab_index;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::type_complexity)]
|
|
||||||
fn handle_click(
|
|
||||||
on_click: &Option<Rc<dyn Fn(&bool, &mut Window, &mut App) + 'static>>,
|
|
||||||
checked: bool,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut App,
|
|
||||||
) {
|
|
||||||
let new_checked = !checked;
|
|
||||||
if let Some(f) = on_click {
|
|
||||||
(f)(&new_checked, window, cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl InteractiveElement for Checkbox {
|
|
||||||
fn interactivity(&mut self) -> &mut gpui::Interactivity {
|
|
||||||
self.base.interactivity()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl StatefulInteractiveElement for Checkbox {}
|
|
||||||
|
|
||||||
impl Styled for Checkbox {
|
|
||||||
fn style(&mut self) -> &mut gpui::StyleRefinement {
|
|
||||||
&mut self.style
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Disableable for Checkbox {
|
|
||||||
fn disabled(mut self, disabled: bool) -> Self {
|
|
||||||
self.disabled = disabled;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Selectable for Checkbox {
|
|
||||||
fn selected(self, selected: bool) -> Self {
|
|
||||||
self.checked(selected)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_selected(&self) -> bool {
|
|
||||||
self.checked
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ParentElement for Checkbox {
|
|
||||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
|
||||||
self.children.extend(elements);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Sizable for Checkbox {
|
|
||||||
fn with_size(mut self, size: impl Into<Size>) -> Self {
|
|
||||||
self.size = size.into();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn checkbox_check_icon(
|
|
||||||
id: ElementId,
|
|
||||||
size: Size,
|
|
||||||
checked: bool,
|
|
||||||
disabled: bool,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut App,
|
|
||||||
) -> impl IntoElement {
|
|
||||||
let toggle_state = window.use_keyed_state(id, cx, |_, _| checked);
|
|
||||||
|
|
||||||
let color = if disabled {
|
|
||||||
cx.theme().text.opacity(0.5)
|
|
||||||
} else {
|
|
||||||
cx.theme().text
|
|
||||||
};
|
|
||||||
|
|
||||||
svg()
|
|
||||||
.absolute()
|
|
||||||
.top_px()
|
|
||||||
.left_px()
|
|
||||||
.map(|this| match size {
|
|
||||||
Size::XSmall => this.size_2(),
|
|
||||||
Size::Small => this.size_2p5(),
|
|
||||||
Size::Medium => this.size_3(),
|
|
||||||
Size::Large => this.size_3p5(),
|
|
||||||
_ => this.size_3(),
|
|
||||||
})
|
|
||||||
.text_color(color)
|
|
||||||
.map(|this| match checked {
|
|
||||||
true => this.path(IconName::Check.path()),
|
|
||||||
_ => this,
|
|
||||||
})
|
|
||||||
.map(|this| {
|
|
||||||
if !disabled && checked != *toggle_state.read(cx) {
|
|
||||||
let duration = Duration::from_secs_f64(0.25);
|
|
||||||
cx.spawn({
|
|
||||||
let toggle_state = toggle_state.clone();
|
|
||||||
async move |cx| {
|
|
||||||
cx.background_executor().timer(duration).await;
|
|
||||||
toggle_state.update(cx, |this, _| *this = checked);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
|
|
||||||
this.with_animation(
|
|
||||||
ElementId::NamedInteger("toggle".into(), checked as u64),
|
|
||||||
Animation::new(Duration::from_secs_f64(0.25)),
|
|
||||||
move |this, delta| {
|
|
||||||
this.opacity(if checked { 1.0 * delta } else { 1.0 - delta })
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.into_any_element()
|
|
||||||
} else {
|
|
||||||
this.into_any_element()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenderOnce for Checkbox {
|
|
||||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
|
||||||
let focus_handle = window
|
|
||||||
.use_keyed_state(self.id.clone(), cx, |_, cx| cx.focus_handle())
|
|
||||||
.read(cx)
|
|
||||||
.clone();
|
|
||||||
|
|
||||||
let checked = self.checked;
|
|
||||||
let radius = cx.theme().radius.min(px(4.));
|
|
||||||
|
|
||||||
let border_color = if checked {
|
|
||||||
cx.theme().border_focused
|
|
||||||
} else {
|
|
||||||
cx.theme().border
|
|
||||||
};
|
|
||||||
|
|
||||||
let color = if self.disabled {
|
|
||||||
border_color.opacity(0.5)
|
|
||||||
} else {
|
|
||||||
border_color
|
|
||||||
};
|
|
||||||
|
|
||||||
div().child(
|
|
||||||
self.base
|
|
||||||
.id(self.id.clone())
|
|
||||||
.when(!self.disabled, |this| {
|
|
||||||
this.track_focus(
|
|
||||||
&focus_handle
|
|
||||||
.tab_stop(self.tab_stop)
|
|
||||||
.tab_index(self.tab_index),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.h_flex()
|
|
||||||
.gap_2()
|
|
||||||
.items_start()
|
|
||||||
.line_height(relative(1.))
|
|
||||||
.text_color(cx.theme().text)
|
|
||||||
.map(|this| match self.size {
|
|
||||||
Size::XSmall => this.text_xs(),
|
|
||||||
Size::Small => this.text_sm(),
|
|
||||||
Size::Medium => this.text_base(),
|
|
||||||
Size::Large => this.text_lg(),
|
|
||||||
_ => this,
|
|
||||||
})
|
|
||||||
.when(self.disabled, |this| this.text_color(cx.theme().text_muted))
|
|
||||||
.rounded(cx.theme().radius * 0.5)
|
|
||||||
.refine_style(&self.style)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.relative()
|
|
||||||
.map(|this| match self.size {
|
|
||||||
Size::XSmall => this.size_3(),
|
|
||||||
Size::Small => this.size_3p5(),
|
|
||||||
Size::Medium => this.size_4(),
|
|
||||||
Size::Large => this.size(rems(1.125)),
|
|
||||||
_ => this.size_4(),
|
|
||||||
})
|
|
||||||
.flex_shrink_0()
|
|
||||||
.border_1()
|
|
||||||
.border_color(color)
|
|
||||||
.rounded(radius)
|
|
||||||
.when(cx.theme().shadow && !self.disabled, |this| this.shadow_xs())
|
|
||||||
.map(|this| match checked {
|
|
||||||
false => this.bg(cx.theme().background),
|
|
||||||
_ => this.bg(color),
|
|
||||||
})
|
|
||||||
.child(checkbox_check_icon(
|
|
||||||
self.id,
|
|
||||||
self.size,
|
|
||||||
checked,
|
|
||||||
self.disabled,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.when(self.label.is_some() || !self.children.is_empty(), |this| {
|
|
||||||
this.child(
|
|
||||||
v_flex()
|
|
||||||
.w_full()
|
|
||||||
.line_height(relative(1.2))
|
|
||||||
.gap_1()
|
|
||||||
.map(|this| {
|
|
||||||
if let Some(label) = self.label {
|
|
||||||
this.child(
|
|
||||||
div()
|
|
||||||
.size_full()
|
|
||||||
.text_color(cx.theme().text)
|
|
||||||
.when(self.disabled, |this| {
|
|
||||||
this.text_color(cx.theme().text_muted)
|
|
||||||
})
|
|
||||||
.line_height(relative(1.))
|
|
||||||
.child(label),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
this
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.children(self.children),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.on_mouse_down(gpui::MouseButton::Left, |_, window, _| {
|
|
||||||
// Avoid focus on mouse down.
|
|
||||||
window.prevent_default();
|
|
||||||
})
|
|
||||||
.when(!self.disabled, |this| {
|
|
||||||
this.on_click({
|
|
||||||
let on_click = self.on_click.clone();
|
|
||||||
move |_, window, cx| {
|
|
||||||
window.prevent_default();
|
|
||||||
Self::handle_click(&on_click, checked, window, cx);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,438 +0,0 @@
|
|||||||
use std::ops::Deref;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use gpui::prelude::FluentBuilder as _;
|
|
||||||
use gpui::{
|
|
||||||
App, AppContext, Axis, Context, Element, Empty, Entity, IntoElement, MouseMoveEvent,
|
|
||||||
MouseUpEvent, ParentElement as _, Pixels, Point, Render, Style, StyleRefinement, Styled as _,
|
|
||||||
WeakEntity, Window, div, px,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{DockArea, DockItem};
|
|
||||||
use crate::StyledExt;
|
|
||||||
use crate::dock::panel::PanelView;
|
|
||||||
use crate::dock::tab_panel::TabPanel;
|
|
||||||
use crate::resizable::{PANEL_MIN_SIZE, resize_handle};
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct ResizePanel;
|
|
||||||
|
|
||||||
impl Render for ResizePanel {
|
|
||||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
Empty
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum DockPlacement {
|
|
||||||
Center,
|
|
||||||
Left,
|
|
||||||
Bottom,
|
|
||||||
Right,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DockPlacement {
|
|
||||||
fn axis(&self) -> Axis {
|
|
||||||
match self {
|
|
||||||
Self::Left | Self::Right => Axis::Horizontal,
|
|
||||||
Self::Bottom => Axis::Vertical,
|
|
||||||
Self::Center => unreachable!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_left(&self) -> bool {
|
|
||||||
matches!(self, Self::Left)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_bottom(&self) -> bool {
|
|
||||||
matches!(self, Self::Bottom)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_right(&self) -> bool {
|
|
||||||
matches!(self, Self::Right)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The Dock is a fixed container that places at left, bottom, right of the Windows.
|
|
||||||
///
|
|
||||||
/// This is unlike Panel, it can't be move or add any other panel.
|
|
||||||
pub struct Dock {
|
|
||||||
pub(super) placement: DockPlacement,
|
|
||||||
dock_area: WeakEntity<DockArea>,
|
|
||||||
|
|
||||||
/// Dock layout
|
|
||||||
pub(crate) panel: DockItem,
|
|
||||||
|
|
||||||
/// The size is means the width or height of the Dock, if the placement is left or right, the size is width, otherwise the size is height.
|
|
||||||
pub(super) size: Pixels,
|
|
||||||
|
|
||||||
/// Whether the Dock is open
|
|
||||||
pub(super) open: bool,
|
|
||||||
|
|
||||||
/// Whether the Dock is collapsible, default: true
|
|
||||||
pub(super) collapsible: bool,
|
|
||||||
|
|
||||||
/// Whether the Dock is resizing
|
|
||||||
resizing: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Dock {
|
|
||||||
pub(crate) fn new(
|
|
||||||
dock_area: WeakEntity<DockArea>,
|
|
||||||
placement: DockPlacement,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) -> Self {
|
|
||||||
let panel = cx.new(|cx| {
|
|
||||||
let mut tab = TabPanel::new(None, dock_area.clone(), window, cx);
|
|
||||||
tab.closable = true;
|
|
||||||
tab
|
|
||||||
});
|
|
||||||
|
|
||||||
let panel = DockItem::Tabs {
|
|
||||||
items: Vec::new(),
|
|
||||||
active_ix: 0,
|
|
||||||
view: panel.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
Self::subscribe_panel_events(dock_area.clone(), &panel, window, cx);
|
|
||||||
|
|
||||||
Self {
|
|
||||||
placement,
|
|
||||||
dock_area,
|
|
||||||
panel,
|
|
||||||
open: true,
|
|
||||||
collapsible: true,
|
|
||||||
size: px(200.0),
|
|
||||||
resizing: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn left(
|
|
||||||
dock_area: WeakEntity<DockArea>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) -> Self {
|
|
||||||
Self::new(dock_area, DockPlacement::Left, window, cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn bottom(
|
|
||||||
dock_area: WeakEntity<DockArea>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) -> Self {
|
|
||||||
Self::new(dock_area, DockPlacement::Bottom, window, cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn right(
|
|
||||||
dock_area: WeakEntity<DockArea>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) -> Self {
|
|
||||||
Self::new(dock_area, DockPlacement::Right, window, cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update the Dock to be collapsible or not.
|
|
||||||
///
|
|
||||||
/// And if the Dock is not collapsible, it will be open.
|
|
||||||
pub fn set_collapsible(
|
|
||||||
&mut self,
|
|
||||||
collapsible: bool,
|
|
||||||
_window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
self.collapsible = collapsible;
|
|
||||||
if !collapsible {
|
|
||||||
self.open = true
|
|
||||||
}
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn subscribe_panel_events(
|
|
||||||
dock_area: WeakEntity<DockArea>,
|
|
||||||
panel: &DockItem,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut App,
|
|
||||||
) {
|
|
||||||
match panel {
|
|
||||||
DockItem::Tabs { view, .. } => {
|
|
||||||
window.defer(cx, {
|
|
||||||
let view = view.clone();
|
|
||||||
move |window, cx| {
|
|
||||||
_ = dock_area.update(cx, |this, cx| {
|
|
||||||
this.subscribe_panel(&view, window, cx);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
DockItem::Split { items, view, .. } => {
|
|
||||||
for item in items {
|
|
||||||
Self::subscribe_panel_events(dock_area.clone(), item, window, cx);
|
|
||||||
}
|
|
||||||
window.defer(cx, {
|
|
||||||
let view = view.clone();
|
|
||||||
move |window, cx| {
|
|
||||||
_ = dock_area.update(cx, |this, cx| {
|
|
||||||
this.subscribe_panel(&view, window, cx);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
DockItem::Panel { .. } => {
|
|
||||||
// Not supported
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_panel(&mut self, panel: DockItem, _window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
self.panel = panel;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_open(&self) -> bool {
|
|
||||||
self.open
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn toggle_open(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
self.set_open(!self.open, window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the size of the Dock, the size is means the width or height of
|
|
||||||
/// the Dock, if the placement is left or right, the size is width,
|
|
||||||
/// otherwise the size is height.
|
|
||||||
pub fn size(&self) -> Pixels {
|
|
||||||
self.size
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the size of the Dock.
|
|
||||||
pub fn set_size(&mut self, size: Pixels, _window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
self.size = size.max(PANEL_MIN_SIZE);
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the open state of the Dock.
|
|
||||||
pub fn set_open(&mut self, open: bool, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
self.open = open;
|
|
||||||
let item = self.panel.clone();
|
|
||||||
// Use defer_in (not window.defer) so the callback is cancelled
|
|
||||||
// if this Dock entity is dropped before the deferred frame runs.
|
|
||||||
cx.defer_in(window, move |_, window, cx| {
|
|
||||||
item.set_collapsed(!open, window, cx);
|
|
||||||
});
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add item to the Dock.
|
|
||||||
pub fn add_panel(
|
|
||||||
&mut self,
|
|
||||||
panel: Arc<dyn PanelView>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
self.panel.add_panel(panel, &self.dock_area, window, cx);
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_resize_handle(
|
|
||||||
&mut self,
|
|
||||||
_window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) -> impl IntoElement {
|
|
||||||
let axis = self.placement.axis();
|
|
||||||
let view = cx.entity().clone();
|
|
||||||
|
|
||||||
resize_handle("resize-handle", axis)
|
|
||||||
.placement(self.placement)
|
|
||||||
.on_drag(ResizePanel {}, move |info, _, _, cx| {
|
|
||||||
cx.stop_propagation();
|
|
||||||
view.update(cx, |view, _cx| {
|
|
||||||
view.resizing = true;
|
|
||||||
});
|
|
||||||
cx.new(|_| info.deref().clone())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resize(
|
|
||||||
&mut self,
|
|
||||||
mouse_position: Point<Pixels>,
|
|
||||||
_window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
if !self.resizing {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let dock_area = self
|
|
||||||
.dock_area
|
|
||||||
.upgrade()
|
|
||||||
.expect("DockArea is missing")
|
|
||||||
.read(cx);
|
|
||||||
|
|
||||||
let area_bounds = dock_area.bounds;
|
|
||||||
let mut left_dock_size = px(0.0);
|
|
||||||
let mut right_dock_size = px(0.0);
|
|
||||||
|
|
||||||
// Get the size of the left dock if it's open and not the current dock
|
|
||||||
if let Some(left_dock) = &dock_area.left_dock
|
|
||||||
&& left_dock.entity_id() != cx.entity().entity_id()
|
|
||||||
{
|
|
||||||
let left_dock_read = left_dock.read(cx);
|
|
||||||
if left_dock_read.is_open() {
|
|
||||||
left_dock_size = left_dock_read.size;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the size of the right dock if it's open and not the current dock
|
|
||||||
if let Some(right_dock) = &dock_area.right_dock
|
|
||||||
&& right_dock.entity_id() != cx.entity().entity_id()
|
|
||||||
{
|
|
||||||
let right_dock_read = right_dock.read(cx);
|
|
||||||
if right_dock_read.is_open() {
|
|
||||||
right_dock_size = right_dock_read.size;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let size = match self.placement {
|
|
||||||
DockPlacement::Left => mouse_position.x - area_bounds.left(),
|
|
||||||
DockPlacement::Right => area_bounds.right() - mouse_position.x,
|
|
||||||
DockPlacement::Bottom => area_bounds.bottom() - mouse_position.y,
|
|
||||||
DockPlacement::Center => unreachable!(),
|
|
||||||
};
|
|
||||||
|
|
||||||
match self.placement {
|
|
||||||
DockPlacement::Left => {
|
|
||||||
let max_size = area_bounds.size.width - PANEL_MIN_SIZE - right_dock_size;
|
|
||||||
self.size = size.clamp(PANEL_MIN_SIZE, max_size);
|
|
||||||
}
|
|
||||||
DockPlacement::Right => {
|
|
||||||
let max_size = area_bounds.size.width - PANEL_MIN_SIZE - left_dock_size;
|
|
||||||
self.size = size.clamp(PANEL_MIN_SIZE, max_size);
|
|
||||||
}
|
|
||||||
DockPlacement::Bottom => {
|
|
||||||
let max_size = area_bounds.size.height - PANEL_MIN_SIZE;
|
|
||||||
self.size = size.clamp(PANEL_MIN_SIZE, max_size);
|
|
||||||
}
|
|
||||||
DockPlacement::Center => unreachable!(),
|
|
||||||
}
|
|
||||||
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn done_resizing(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {
|
|
||||||
self.resizing = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for Dock {
|
|
||||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl gpui::IntoElement {
|
|
||||||
if !self.open && !self.placement.is_bottom() {
|
|
||||||
return div();
|
|
||||||
}
|
|
||||||
|
|
||||||
let cache_style = StyleRefinement::default().absolute().size_full();
|
|
||||||
|
|
||||||
div()
|
|
||||||
.relative()
|
|
||||||
.overflow_hidden()
|
|
||||||
.map(|this| match self.placement {
|
|
||||||
DockPlacement::Left | DockPlacement::Right => this.h_flex().h_full().w(self.size),
|
|
||||||
DockPlacement::Bottom => this.w_full().h(self.size),
|
|
||||||
DockPlacement::Center => unreachable!(),
|
|
||||||
})
|
|
||||||
// Bottom Dock should keep the title bar, then user can click the Toggle button
|
|
||||||
.when(!self.open && self.placement.is_bottom(), |this| {
|
|
||||||
this.h(px(29.))
|
|
||||||
})
|
|
||||||
.map(|this| match &self.panel {
|
|
||||||
DockItem::Split { view, .. } => this.child(view.clone()),
|
|
||||||
DockItem::Tabs { view, .. } => this.child(view.clone()),
|
|
||||||
DockItem::Panel { view, .. } => this.child(view.clone().view().cached(cache_style)),
|
|
||||||
})
|
|
||||||
.child(self.render_resize_handle(window, cx))
|
|
||||||
.child(DockElement {
|
|
||||||
view: cx.entity().clone(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct DockElement {
|
|
||||||
view: Entity<Dock>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IntoElement for DockElement {
|
|
||||||
type Element = Self;
|
|
||||||
|
|
||||||
fn into_element(self) -> Self::Element {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Element for DockElement {
|
|
||||||
type PrepaintState = ();
|
|
||||||
type RequestLayoutState = ();
|
|
||||||
|
|
||||||
fn id(&self) -> Option<gpui::ElementId> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn request_layout(
|
|
||||||
&mut self,
|
|
||||||
_: Option<&gpui::GlobalElementId>,
|
|
||||||
_: Option<&gpui::InspectorElementId>,
|
|
||||||
window: &mut gpui::Window,
|
|
||||||
cx: &mut App,
|
|
||||||
) -> (gpui::LayoutId, Self::RequestLayoutState) {
|
|
||||||
(window.request_layout(Style::default(), None, cx), ())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prepaint(
|
|
||||||
&mut self,
|
|
||||||
_: Option<&gpui::GlobalElementId>,
|
|
||||||
_: Option<&gpui::InspectorElementId>,
|
|
||||||
_: gpui::Bounds<Pixels>,
|
|
||||||
_: &mut Self::RequestLayoutState,
|
|
||||||
_window: &mut Window,
|
|
||||||
_cx: &mut App,
|
|
||||||
) -> Self::PrepaintState {
|
|
||||||
}
|
|
||||||
|
|
||||||
fn paint(
|
|
||||||
&mut self,
|
|
||||||
_: Option<&gpui::GlobalElementId>,
|
|
||||||
_: Option<&gpui::InspectorElementId>,
|
|
||||||
_: gpui::Bounds<Pixels>,
|
|
||||||
_: &mut Self::RequestLayoutState,
|
|
||||||
_: &mut Self::PrepaintState,
|
|
||||||
window: &mut gpui::Window,
|
|
||||||
cx: &mut App,
|
|
||||||
) {
|
|
||||||
window.on_mouse_event({
|
|
||||||
let view = self.view.clone();
|
|
||||||
let is_resizing = view.read(cx).resizing;
|
|
||||||
move |e: &MouseMoveEvent, phase, window, cx| {
|
|
||||||
if !is_resizing {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if !phase.bubble() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
view.update(cx, |view, cx| view.resize(e.position, window, cx))
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// When any mouse up, stop dragging
|
|
||||||
window.on_mouse_event({
|
|
||||||
let view = self.view.clone();
|
|
||||||
move |_: &MouseUpEvent, phase, window, cx| {
|
|
||||||
if phase.bubble() {
|
|
||||||
view.update(cx, |view, cx| view.done_resizing(window, cx));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+833
-737
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,11 @@
|
|||||||
|
use std::any::Any;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, AnyView, App, Element, Entity, EventEmitter, FocusHandle, Focusable, Render,
|
AnyElement, AnyView, App, Element, Entity, EventEmitter, FocusHandle, Focusable, Render,
|
||||||
SharedString, Window,
|
SharedString, Window,
|
||||||
};
|
};
|
||||||
|
use gpui_base::dock::{PanelId, PanelState};
|
||||||
|
|
||||||
use crate::button::Button;
|
use crate::button::Button;
|
||||||
use crate::menu::PopupMenu;
|
use crate::menu::PopupMenu;
|
||||||
@@ -13,14 +17,6 @@ pub enum PanelEvent {
|
|||||||
LayoutChanged,
|
LayoutChanged,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum PanelStyle {
|
|
||||||
/// Display the TabBar when there are multiple tabs, otherwise display the simple title.
|
|
||||||
Default,
|
|
||||||
/// Always display the tab bar.
|
|
||||||
TabBar,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait Panel: EventEmitter<PanelEvent> + Render + Focusable {
|
pub trait Panel: EventEmitter<PanelEvent> + Render + Focusable {
|
||||||
/// The name of the panel used to serialize, deserialize and identify the panel.
|
/// The name of the panel used to serialize, deserialize and identify the panel.
|
||||||
///
|
///
|
||||||
@@ -156,3 +152,84 @@ impl PartialEq for dyn PanelView {
|
|||||||
self.view() == other.view()
|
self.view() == other.view()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct PanelHandle {
|
||||||
|
id: PanelId,
|
||||||
|
panel: Arc<dyn PanelView>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PanelHandle {
|
||||||
|
pub fn new<P: Panel>(panel: Entity<P>) -> Self {
|
||||||
|
Self {
|
||||||
|
id: PanelId::from(panel.entity_id()),
|
||||||
|
panel: Arc::new(panel),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recover the coop handle behind one of base's.
|
||||||
|
pub fn of(panel: &Arc<dyn gpui_base::dock::PanelView>) -> Option<&Self> {
|
||||||
|
panel.as_any().downcast_ref::<Self>()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The coop panel behind this handle.
|
||||||
|
pub fn panel(&self) -> &Arc<dyn PanelView> {
|
||||||
|
&self.panel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl gpui_base::dock::PanelView for PanelHandle {
|
||||||
|
fn panel_name(&self, _: &App) -> &'static str {
|
||||||
|
"CoopPanel"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn panel_id(&self, _: &App) -> PanelId {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn closable(&self, cx: &App) -> bool {
|
||||||
|
self.panel.closable(cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn zoomable(&self, cx: &App) -> bool {
|
||||||
|
self.panel.zoomable(cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visible(&self, cx: &App) -> bool {
|
||||||
|
self.panel.visible(cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_active(&self, active: bool, _: &mut Window, cx: &mut App) {
|
||||||
|
self.panel.set_active(active, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_zoomed(&self, zoomed: bool, _: &mut Window, cx: &mut App) {
|
||||||
|
self.panel.set_zoomed(zoomed, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_added_to(
|
||||||
|
&self,
|
||||||
|
_group: gpui::WeakEntity<gpui_base::dock::TabGroup>,
|
||||||
|
_: &mut Window,
|
||||||
|
_: &mut App,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_removed(&self, _: &mut Window, _: &mut App) {}
|
||||||
|
|
||||||
|
fn view(&self) -> AnyView {
|
||||||
|
self.panel.view()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||||
|
self.panel.focus_handle(cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dump(&self, cx: &App) -> PanelState {
|
||||||
|
PanelState::new(self.panel_name(cx))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_any(&self) -> &dyn Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,387 +0,0 @@
|
|||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use gpui::{
|
|
||||||
App, AppContext, Axis, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
|
|
||||||
IntoElement, ParentElement, Pixels, Render, SharedString, Styled, Subscription, WeakEntity,
|
|
||||||
Window,
|
|
||||||
};
|
|
||||||
use smallvec::SmallVec;
|
|
||||||
use theme::{AxisExt as _, Placement};
|
|
||||||
|
|
||||||
use super::{DockArea, PanelEvent};
|
|
||||||
use crate::dock::panel::{Panel, PanelView};
|
|
||||||
use crate::dock::tab_panel::TabPanel;
|
|
||||||
use crate::h_flex;
|
|
||||||
use crate::resizable::{
|
|
||||||
PANEL_MIN_SIZE, ResizablePanelEvent, ResizablePanelGroup, ResizablePanelState, ResizableState,
|
|
||||||
resizable_panel,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct StackPanel {
|
|
||||||
pub(super) parent: Option<WeakEntity<StackPanel>>,
|
|
||||||
pub(super) axis: Axis,
|
|
||||||
focus_handle: FocusHandle,
|
|
||||||
pub(crate) panels: SmallVec<[Arc<dyn PanelView>; 2]>,
|
|
||||||
state: Entity<ResizableState>,
|
|
||||||
_subscriptions: Vec<Subscription>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Panel for StackPanel {
|
|
||||||
fn panel_id(&self) -> SharedString {
|
|
||||||
"StackPanel".into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn title(&self, _cx: &App) -> gpui::AnyElement {
|
|
||||||
"StackPanel".into_any_element()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl StackPanel {
|
|
||||||
pub fn new(axis: Axis, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
|
||||||
let state = cx.new(|_| ResizableState::default());
|
|
||||||
|
|
||||||
// Bubble up the resize event.
|
|
||||||
let subscriptions =
|
|
||||||
vec![
|
|
||||||
cx.subscribe_in(&state, window, |_, _, _: &ResizablePanelEvent, _, cx| {
|
|
||||||
cx.emit(PanelEvent::LayoutChanged)
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
|
|
||||||
Self {
|
|
||||||
axis,
|
|
||||||
parent: None,
|
|
||||||
focus_handle: cx.focus_handle(),
|
|
||||||
panels: SmallVec::new(),
|
|
||||||
state,
|
|
||||||
_subscriptions: subscriptions,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The first level of the stack panel is root, will not have a parent.
|
|
||||||
fn is_root(&self) -> bool {
|
|
||||||
self.parent.is_none()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return true if self or parent only have last panel.
|
|
||||||
pub fn is_last_panel(&self, cx: &App) -> bool {
|
|
||||||
if self.panels.len() > 1 {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(parent) = &self.parent
|
|
||||||
&& let Some(parent) = parent.upgrade()
|
|
||||||
{
|
|
||||||
return parent.read(cx).is_last_panel(cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn panels_len(&self) -> usize {
|
|
||||||
self.panels.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return the index of the panel.
|
|
||||||
pub fn index_of_panel(&self, panel: Arc<dyn PanelView>) -> Option<usize> {
|
|
||||||
self.panels.iter().position(|p| p == &panel)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add a panel at the end of the stack.
|
|
||||||
pub fn add_panel(
|
|
||||||
&mut self,
|
|
||||||
panel: Arc<dyn PanelView>,
|
|
||||||
size: Option<Pixels>,
|
|
||||||
dock_area: WeakEntity<DockArea>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
self.insert_panel(panel, self.panels.len(), size, dock_area, window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_panel_at(
|
|
||||||
&mut self,
|
|
||||||
panel: Arc<dyn PanelView>,
|
|
||||||
placement: Placement,
|
|
||||||
size: Option<Pixels>,
|
|
||||||
dock_area: WeakEntity<DockArea>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
self.insert_panel_at(
|
|
||||||
panel,
|
|
||||||
self.panels_len(),
|
|
||||||
placement,
|
|
||||||
size,
|
|
||||||
dock_area,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
pub fn insert_panel_at(
|
|
||||||
&mut self,
|
|
||||||
panel: Arc<dyn PanelView>,
|
|
||||||
ix: usize,
|
|
||||||
placement: Placement,
|
|
||||||
size: Option<Pixels>,
|
|
||||||
dock_area: WeakEntity<DockArea>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
match placement {
|
|
||||||
Placement::Top | Placement::Left => {
|
|
||||||
self.insert_panel_before(panel, ix, size, dock_area, window, cx)
|
|
||||||
}
|
|
||||||
Placement::Right | Placement::Bottom => {
|
|
||||||
self.insert_panel_after(panel, ix, size, dock_area, window, cx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Insert a panel at the index.
|
|
||||||
pub fn insert_panel_before(
|
|
||||||
&mut self,
|
|
||||||
panel: Arc<dyn PanelView>,
|
|
||||||
ix: usize,
|
|
||||||
size: Option<Pixels>,
|
|
||||||
dock_area: WeakEntity<DockArea>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
self.insert_panel(panel, ix, size, dock_area, window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Insert a panel after the index.
|
|
||||||
pub fn insert_panel_after(
|
|
||||||
&mut self,
|
|
||||||
panel: Arc<dyn PanelView>,
|
|
||||||
ix: usize,
|
|
||||||
size: Option<Pixels>,
|
|
||||||
dock_area: WeakEntity<DockArea>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
self.insert_panel(panel, ix + 1, size, dock_area, window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn insert_panel(
|
|
||||||
&mut self,
|
|
||||||
panel: Arc<dyn PanelView>,
|
|
||||||
ix: usize,
|
|
||||||
size: Option<Pixels>,
|
|
||||||
dock_area: WeakEntity<DockArea>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
// If the panel is already in the stack, return.
|
|
||||||
if self.index_of_panel(panel.clone()).is_some() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let view = cx.entity().clone();
|
|
||||||
|
|
||||||
window.defer(cx, {
|
|
||||||
let panel = panel.clone();
|
|
||||||
|
|
||||||
move |window, cx| {
|
|
||||||
// If the panel is a TabPanel, set its parent to this.
|
|
||||||
if let Ok(tab_panel) = panel.view().downcast::<TabPanel>() {
|
|
||||||
tab_panel.update(cx, |tab_panel, _| tab_panel.set_parent(view.downgrade()));
|
|
||||||
} else if let Ok(stack_panel) = panel.view().downcast::<Self>() {
|
|
||||||
stack_panel.update(cx, |stack_panel, _| {
|
|
||||||
stack_panel.parent = Some(view.downgrade())
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subscribe to the panel's layout change event.
|
|
||||||
_ = dock_area.update(cx, |this, cx| {
|
|
||||||
if let Ok(tab_panel) = panel.view().downcast::<TabPanel>() {
|
|
||||||
this.subscribe_panel(&tab_panel, window, cx);
|
|
||||||
} else if let Ok(stack_panel) = panel.view().downcast::<Self>() {
|
|
||||||
this.subscribe_panel(&stack_panel, window, cx);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let ix = if ix > self.panels.len() {
|
|
||||||
self.panels.len()
|
|
||||||
} else {
|
|
||||||
ix
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get avg size of all panels to insert new panel, if size is None.
|
|
||||||
let size = match size {
|
|
||||||
Some(size) => size,
|
|
||||||
None => {
|
|
||||||
let state = self.state.read(cx);
|
|
||||||
(state.container_size() / (state.sizes().len() + 1) as f32).max(PANEL_MIN_SIZE)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Insert panel
|
|
||||||
self.panels.insert(ix, panel.clone());
|
|
||||||
|
|
||||||
// Update resizable state
|
|
||||||
self.state.update(cx, |state, cx| {
|
|
||||||
state.insert_panel(Some(size), Some(ix), cx);
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.emit(PanelEvent::LayoutChanged);
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove panel from the stack.
|
|
||||||
///
|
|
||||||
/// If `ix` is not found, do nothing.
|
|
||||||
pub fn remove_panel(
|
|
||||||
&mut self,
|
|
||||||
panel: Arc<dyn PanelView>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
let Some(ix) = self.index_of_panel(panel.clone()) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
self.panels.remove(ix);
|
|
||||||
self.state.update(cx, |state, cx| {
|
|
||||||
state.remove_panel(ix, cx);
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.emit(PanelEvent::LayoutChanged);
|
|
||||||
|
|
||||||
self.remove_self_if_empty(window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Replace the old panel with the new panel at same index.
|
|
||||||
pub fn replace_panel(
|
|
||||||
&mut self,
|
|
||||||
old_panel: Arc<dyn PanelView>,
|
|
||||||
new_panel: Entity<StackPanel>,
|
|
||||||
_window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
if let Some(ix) = self.index_of_panel(old_panel.clone()) {
|
|
||||||
self.panels[ix] = Arc::new(new_panel.clone());
|
|
||||||
self.state.update(cx, |state, cx| {
|
|
||||||
state.replace_panel(ix, ResizablePanelState::default(), cx);
|
|
||||||
});
|
|
||||||
cx.emit(PanelEvent::LayoutChanged);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// If children is empty, remove self from parent view.
|
|
||||||
pub fn remove_self_if_empty(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
if self.is_root() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.panels.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let view = cx.entity().clone();
|
|
||||||
if let Some(parent) = self.parent.as_ref() {
|
|
||||||
_ = parent.update(cx, |parent, cx| {
|
|
||||||
parent.remove_panel(Arc::new(view.clone()), window, cx);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
cx.emit(PanelEvent::LayoutChanged);
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Find the first top left in the stack.
|
|
||||||
pub fn left_top_tab_panel(&self, check_parent: bool, cx: &App) -> Option<Entity<TabPanel>> {
|
|
||||||
if check_parent
|
|
||||||
&& let Some(parent) = self.parent.as_ref().and_then(|parent| parent.upgrade())
|
|
||||||
&& let Some(panel) = parent.read(cx).left_top_tab_panel(true, cx)
|
|
||||||
{
|
|
||||||
return Some(panel);
|
|
||||||
}
|
|
||||||
|
|
||||||
let first_panel = self.panels.first();
|
|
||||||
if let Some(view) = first_panel {
|
|
||||||
if let Ok(tab_panel) = view.view().downcast::<TabPanel>() {
|
|
||||||
Some(tab_panel)
|
|
||||||
} else if let Ok(stack_panel) = view.view().downcast::<StackPanel>() {
|
|
||||||
stack_panel.read(cx).left_top_tab_panel(false, cx)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Find the first top right in the stack.
|
|
||||||
pub fn right_top_tab_panel(&self, check_parent: bool, cx: &App) -> Option<Entity<TabPanel>> {
|
|
||||||
if check_parent
|
|
||||||
&& let Some(parent) = self.parent.as_ref().and_then(|parent| parent.upgrade())
|
|
||||||
&& let Some(panel) = parent.read(cx).right_top_tab_panel(true, cx)
|
|
||||||
{
|
|
||||||
return Some(panel);
|
|
||||||
}
|
|
||||||
|
|
||||||
let panel = if self.axis.is_vertical() {
|
|
||||||
self.panels.first()
|
|
||||||
} else {
|
|
||||||
self.panels.last()
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(view) = panel {
|
|
||||||
if let Ok(tab_panel) = view.view().downcast::<TabPanel>() {
|
|
||||||
Some(tab_panel)
|
|
||||||
} else if let Ok(stack_panel) = view.view().downcast::<StackPanel>() {
|
|
||||||
stack_panel.read(cx).right_top_tab_panel(false, cx)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove all panels from the stack.
|
|
||||||
pub fn remove_all_panels(&mut self, _: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
self.panels.clear();
|
|
||||||
self.state.update(cx, |state, cx| {
|
|
||||||
state.clear();
|
|
||||||
cx.notify();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Change the axis of the stack panel.
|
|
||||||
pub fn set_axis(&mut self, axis: Axis, _: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
self.axis = axis;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Focusable for StackPanel {
|
|
||||||
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
|
||||||
self.focus_handle.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EventEmitter<PanelEvent> for StackPanel {}
|
|
||||||
|
|
||||||
impl EventEmitter<DismissEvent> for StackPanel {}
|
|
||||||
|
|
||||||
impl Render for StackPanel {
|
|
||||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
h_flex().size_full().overflow_hidden().child(
|
|
||||||
ResizablePanelGroup::new("stack-panel-group")
|
|
||||||
.with_state(&self.state)
|
|
||||||
.axis(self.axis)
|
|
||||||
.children(self.panels.clone().into_iter().map(|panel| {
|
|
||||||
resizable_panel()
|
|
||||||
.child(panel.view())
|
|
||||||
.visible(panel.visible(cx))
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,27 +0,0 @@
|
|||||||
use gpui::{canvas, App, Bounds, ParentElement, Pixels, Styled as _, Window};
|
|
||||||
|
|
||||||
/// A trait to extend [`gpui::Element`] with additional functionality.
|
|
||||||
pub trait ElementExt: ParentElement + Sized {
|
|
||||||
/// Add a prepaint callback to the element.
|
|
||||||
///
|
|
||||||
/// This is a helper method to get the bounds of the element after paint.
|
|
||||||
///
|
|
||||||
/// The first argument is the bounds of the element in pixels.
|
|
||||||
///
|
|
||||||
/// See also [`gpui::canvas`].
|
|
||||||
fn on_prepaint<F>(self, f: F) -> Self
|
|
||||||
where
|
|
||||||
F: FnOnce(Bounds<Pixels>, &mut Window, &mut App) + 'static,
|
|
||||||
{
|
|
||||||
self.child(
|
|
||||||
canvas(
|
|
||||||
move |bounds, window, cx| f(bounds, window, cx),
|
|
||||||
|_, _, _, _| {},
|
|
||||||
)
|
|
||||||
.absolute()
|
|
||||||
.size_full(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: ParentElement> ElementExt for T {}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
use gpui::{App, ClickEvent, InteractiveElement, Stateful, Window};
|
|
||||||
|
|
||||||
pub trait InteractiveElementExt: InteractiveElement {
|
|
||||||
/// Set the listener for a double click event.
|
|
||||||
fn on_double_click(
|
|
||||||
mut self,
|
|
||||||
listener: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
|
||||||
) -> Self
|
|
||||||
where
|
|
||||||
Self: Sized,
|
|
||||||
{
|
|
||||||
self.interactivity().on_click(move |event, window, cx| {
|
|
||||||
if event.click_count() == 2 {
|
|
||||||
listener(event, window, cx);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<E: InteractiveElement> InteractiveElementExt for Stateful<E> {}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
use gpui::{Context, FocusHandle, Window};
|
|
||||||
|
|
||||||
/// A trait for views that can cycle focus between its children.
|
|
||||||
///
|
|
||||||
/// This will provide a default implementation for the `cycle_focus` method that will cycle focus.
|
|
||||||
///
|
|
||||||
/// You should implement the `cycle_focus_handles` method to return a list of focus handles that
|
|
||||||
/// should be cycled, and the cycle will follow the order of the list.
|
|
||||||
pub trait FocusableCycle {
|
|
||||||
/// Returns a list of focus handles that should be cycled.
|
|
||||||
fn cycle_focus_handles(&self, window: &mut Window, cx: &mut Context<Self>) -> Vec<FocusHandle>
|
|
||||||
where
|
|
||||||
Self: Sized;
|
|
||||||
|
|
||||||
/// Cycles focus between the focus handles returned by `cycle_focus_handles`.
|
|
||||||
/// If `is_next` is `true`, it will cycle to the next focus handle, otherwise it will cycle to prev.
|
|
||||||
fn cycle_focus(&self, is_next: bool, window: &mut Window, cx: &mut Context<Self>)
|
|
||||||
where
|
|
||||||
Self: Sized,
|
|
||||||
{
|
|
||||||
let focused_handle = window.focused(cx);
|
|
||||||
let handles = self.cycle_focus_handles(window, cx);
|
|
||||||
let handles = if is_next {
|
|
||||||
handles
|
|
||||||
} else {
|
|
||||||
handles.into_iter().rev().collect()
|
|
||||||
};
|
|
||||||
|
|
||||||
let fallback_handle = handles[0].clone();
|
|
||||||
let target_focus_handle = handles
|
|
||||||
.into_iter()
|
|
||||||
.skip_while(|handle| Some(handle) != focused_handle.as_ref())
|
|
||||||
.nth(1)
|
|
||||||
.unwrap_or(fallback_handle);
|
|
||||||
|
|
||||||
target_focus_handle.focus(window, cx);
|
|
||||||
cx.stop_propagation();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,184 +0,0 @@
|
|||||||
use std::fmt::Debug;
|
|
||||||
use instant::{Duration, Instant};
|
|
||||||
|
|
||||||
/// A HistoryItem represents a single change in the history.
|
|
||||||
/// It must implement Clone and PartialEq to be used in the History.
|
|
||||||
pub trait HistoryItem: Clone + PartialEq {
|
|
||||||
fn version(&self) -> usize;
|
|
||||||
fn set_version(&mut self, version: usize);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The History is used to keep track of changes to a model and to allow undo and redo operations.
|
|
||||||
///
|
|
||||||
/// This is now used in Input for undo/redo operations. You can also use this in
|
|
||||||
/// your own models to keep track of changes, for example to track the tab
|
|
||||||
/// history for prev/next features.
|
|
||||||
///
|
|
||||||
/// ## Use cases
|
|
||||||
///
|
|
||||||
/// - Undo/redo operations in Input
|
|
||||||
/// - Tracking tab history for prev/next features
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct History<I: HistoryItem> {
|
|
||||||
undos: Vec<I>,
|
|
||||||
redos: Vec<I>,
|
|
||||||
last_changed_at: Instant,
|
|
||||||
version: usize,
|
|
||||||
pub(crate) ignore: bool,
|
|
||||||
max_undos: usize,
|
|
||||||
group_interval: Option<Duration>,
|
|
||||||
grouping: bool,
|
|
||||||
unique: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<I> History<I>
|
|
||||||
where
|
|
||||||
I: HistoryItem,
|
|
||||||
{
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
undos: Default::default(),
|
|
||||||
redos: Default::default(),
|
|
||||||
ignore: false,
|
|
||||||
last_changed_at: Instant::now(),
|
|
||||||
version: 0,
|
|
||||||
max_undos: 1000,
|
|
||||||
group_interval: None,
|
|
||||||
grouping: false,
|
|
||||||
unique: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the maximum number of undo steps to keep, defaults to 1000.
|
|
||||||
pub fn max_undos(mut self, max_undos: usize) -> Self {
|
|
||||||
self.max_undos = max_undos;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the history to be unique, defaults to false.
|
|
||||||
/// If set to true, the history will only keep unique changes.
|
|
||||||
pub fn unique(mut self) -> Self {
|
|
||||||
self.unique = true;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the interval in milliseconds to group changes, defaults to None.
|
|
||||||
pub fn group_interval(mut self, group_interval: Duration) -> Self {
|
|
||||||
self.group_interval = Some(group_interval);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Start grouping changes, this will prevent the version from being incremented until `end_grouping` is called.
|
|
||||||
pub fn start_grouping(&mut self) {
|
|
||||||
self.grouping = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// End grouping changes, this will allow the version to be incremented again.
|
|
||||||
pub fn end_grouping(&mut self) {
|
|
||||||
self.grouping = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Increment the version number if the last change was made more than `GROUP_INTERVAL` milliseconds ago.
|
|
||||||
fn inc_version(&mut self) -> usize {
|
|
||||||
let t = Instant::now();
|
|
||||||
if !self.grouping && Some(self.last_changed_at.elapsed()) > self.group_interval {
|
|
||||||
self.version += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.last_changed_at = t;
|
|
||||||
self.version
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the current version number.
|
|
||||||
pub fn version(&self) -> usize {
|
|
||||||
self.version
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Push a new change to the history.
|
|
||||||
pub fn push(&mut self, item: I) {
|
|
||||||
let version = self.inc_version();
|
|
||||||
|
|
||||||
if self.undos.len() >= self.max_undos {
|
|
||||||
self.undos.remove(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.unique {
|
|
||||||
self.undos.retain(|c| *c != item);
|
|
||||||
self.redos.retain(|c| *c != item);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut item = item;
|
|
||||||
item.set_version(version);
|
|
||||||
self.undos.push(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the undo stack.
|
|
||||||
pub fn undos(&self) -> &Vec<I> {
|
|
||||||
&self.undos
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the redo stack.
|
|
||||||
pub fn redos(&self) -> &Vec<I> {
|
|
||||||
&self.redos
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clear the undo and redo stacks.
|
|
||||||
pub fn clear(&mut self) {
|
|
||||||
self.undos.clear();
|
|
||||||
self.redos.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Undo the last change and return the changes that were undone.
|
|
||||||
pub fn undo(&mut self) -> Option<Vec<I>> {
|
|
||||||
if let Some(first_change) = self.undos.pop() {
|
|
||||||
let mut changes = vec![first_change.clone()];
|
|
||||||
// pick the next all changes with the same version
|
|
||||||
while self
|
|
||||||
.undos
|
|
||||||
.iter()
|
|
||||||
.filter(|c| c.version() == first_change.version())
|
|
||||||
.count()
|
|
||||||
> 0
|
|
||||||
{
|
|
||||||
let change = self.undos.pop().unwrap();
|
|
||||||
changes.push(change);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.redos.extend(changes.clone());
|
|
||||||
Some(changes)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Redo the last undone change and return the changes that were redone.
|
|
||||||
pub fn redo(&mut self) -> Option<Vec<I>> {
|
|
||||||
if let Some(first_change) = self.redos.pop() {
|
|
||||||
let mut changes = vec![first_change.clone()];
|
|
||||||
// pick the next all changes with the same version
|
|
||||||
while self
|
|
||||||
.redos
|
|
||||||
.iter()
|
|
||||||
.filter(|c| c.version() == first_change.version())
|
|
||||||
.count()
|
|
||||||
> 0
|
|
||||||
{
|
|
||||||
let change = self.redos.pop().unwrap();
|
|
||||||
changes.push(change);
|
|
||||||
}
|
|
||||||
self.undos.extend(changes.clone());
|
|
||||||
Some(changes)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<I> Default for History<I>
|
|
||||||
where
|
|
||||||
I: HistoryItem,
|
|
||||||
{
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
use std::fmt::{Debug, Display};
|
|
||||||
|
|
||||||
use gpui::ElementId;
|
|
||||||
|
|
||||||
/// Represents an index path in a list, which consists of a section index,
|
|
||||||
///
|
|
||||||
/// The default values for section, row, and column are all set to 0.
|
|
||||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
|
||||||
pub struct IndexPath {
|
|
||||||
/// The section index.
|
|
||||||
pub section: usize,
|
|
||||||
/// The item index in the section.
|
|
||||||
pub row: usize,
|
|
||||||
/// The column index.
|
|
||||||
pub column: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<IndexPath> for ElementId {
|
|
||||||
fn from(path: IndexPath) -> Self {
|
|
||||||
ElementId::Name(format!("index-path({},{},{})", path.section, path.row, path.column).into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for IndexPath {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"IndexPath(section: {}, row: {}, column: {})",
|
|
||||||
self.section, self.row, self.column
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IndexPath {
|
|
||||||
/// Create a new index path with the specified section and row.
|
|
||||||
///
|
|
||||||
/// The `section` is set to 0 by default.
|
|
||||||
/// The `column` is set to 0 by default.
|
|
||||||
pub fn new(row: usize) -> Self {
|
|
||||||
IndexPath {
|
|
||||||
section: 0,
|
|
||||||
row,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the section for the index path.
|
|
||||||
pub fn section(mut self, section: usize) -> Self {
|
|
||||||
self.section = section;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the row for the index path.
|
|
||||||
pub fn row(mut self, row: usize) -> Self {
|
|
||||||
self.row = row;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the column for the index path.
|
|
||||||
pub fn column(mut self, column: usize) -> Self {
|
|
||||||
self.column = column;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if the self is equal to the given index path (Same section and row).
|
|
||||||
pub fn eq_row(&self, index: IndexPath) -> bool {
|
|
||||||
self.section == index.section && self.row == index.row
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
use instant::Duration;
|
|
||||||
|
|
||||||
use gpui::{Context, Pixels, Task, px};
|
|
||||||
|
|
||||||
static INTERVAL: Duration = Duration::from_millis(500);
|
|
||||||
static PAUSE_DELAY: Duration = Duration::from_millis(300);
|
|
||||||
|
|
||||||
// On Windows, Linux, we should use integer to avoid blurry cursor.
|
|
||||||
#[cfg(not(target_os = "macos"))]
|
|
||||||
pub(super) const CURSOR_WIDTH: Pixels = px(2.);
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
pub(super) const CURSOR_WIDTH: Pixels = px(1.5);
|
|
||||||
|
|
||||||
/// To manage the Input cursor blinking.
|
|
||||||
///
|
|
||||||
/// It will start blinking with a interval of 500ms.
|
|
||||||
/// Every loop will notify the view to update the `visible`, and Input will observe this update to touch repaint.
|
|
||||||
///
|
|
||||||
/// The input painter will check if this in visible state, then it will draw the cursor.
|
|
||||||
pub(crate) struct BlinkCursor {
|
|
||||||
visible: bool,
|
|
||||||
paused: bool,
|
|
||||||
epoch: usize,
|
|
||||||
|
|
||||||
_task: Task<()>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BlinkCursor {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
visible: false,
|
|
||||||
paused: false,
|
|
||||||
epoch: 0,
|
|
||||||
_task: Task::ready(()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Start the blinking
|
|
||||||
pub fn start(&mut self, cx: &mut Context<Self>) {
|
|
||||||
self.blink(self.epoch, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn stop(&mut self, cx: &mut Context<Self>) {
|
|
||||||
self.epoch = 0;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn next_epoch(&mut self) -> usize {
|
|
||||||
self.epoch += 1;
|
|
||||||
self.epoch
|
|
||||||
}
|
|
||||||
|
|
||||||
fn blink(&mut self, epoch: usize, cx: &mut Context<Self>) {
|
|
||||||
if self.paused || epoch != self.epoch {
|
|
||||||
self.visible = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.visible = !self.visible;
|
|
||||||
cx.notify();
|
|
||||||
|
|
||||||
// Schedule the next blink
|
|
||||||
let epoch = self.next_epoch();
|
|
||||||
self._task = cx.spawn(async move |this, cx| {
|
|
||||||
cx.background_executor().timer(INTERVAL).await;
|
|
||||||
if let Some(this) = this.upgrade() {
|
|
||||||
this.update(cx, |this, cx| this.blink(epoch, cx));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn visible(&self) -> bool {
|
|
||||||
// Keep showing the cursor if paused
|
|
||||||
self.paused || self.visible
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Pause the blinking, and delay 500ms to resume the blinking.
|
|
||||||
pub fn pause(&mut self, cx: &mut Context<Self>) {
|
|
||||||
self.paused = true;
|
|
||||||
self.visible = true;
|
|
||||||
cx.notify();
|
|
||||||
|
|
||||||
// delay 500ms to start the blinking
|
|
||||||
let epoch = self.next_epoch();
|
|
||||||
self._task = cx.spawn(async move |this, cx| {
|
|
||||||
cx.background_executor().timer(PAUSE_DELAY).await;
|
|
||||||
|
|
||||||
if let Some(this) = this.upgrade() {
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.paused = false;
|
|
||||||
this.blink(epoch, cx);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
use std::fmt::Debug;
|
|
||||||
|
|
||||||
use crate::{history::HistoryItem, input::Selection};
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone)]
|
|
||||||
pub struct Change {
|
|
||||||
pub(crate) old_range: Selection,
|
|
||||||
pub(crate) old_text: String,
|
|
||||||
pub(crate) new_range: Selection,
|
|
||||||
pub(crate) new_text: String,
|
|
||||||
version: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Change {
|
|
||||||
pub fn new(
|
|
||||||
old_range: impl Into<Selection>,
|
|
||||||
old_text: &str,
|
|
||||||
new_range: impl Into<Selection>,
|
|
||||||
new_text: &str,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
old_range: old_range.into(),
|
|
||||||
old_text: old_text.to_string(),
|
|
||||||
new_range: new_range.into(),
|
|
||||||
new_text: new_text.to_string(),
|
|
||||||
version: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HistoryItem for Change {
|
|
||||||
fn version(&self) -> usize {
|
|
||||||
self.version
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_version(&mut self, version: usize) {
|
|
||||||
self.version = version;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
use std::ops::{Range, RangeBounds};
|
|
||||||
|
|
||||||
/// A selection in the text, represented by start and end byte indices.
|
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
|
|
||||||
pub struct Selection {
|
|
||||||
pub start: usize,
|
|
||||||
pub end: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Selection {
|
|
||||||
pub fn new(start: usize, end: usize) -> Self {
|
|
||||||
Self { start, end }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn len(&self) -> usize {
|
|
||||||
self.end.saturating_sub(self.start)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_empty(&self) -> bool {
|
|
||||||
self.start == self.end
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clears the selection, setting start and end to 0.
|
|
||||||
pub fn clear(&mut self) {
|
|
||||||
self.start = 0;
|
|
||||||
self.end = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks if the given offset is within the selection range.
|
|
||||||
pub fn contains(&self, offset: usize) -> bool {
|
|
||||||
offset >= self.start && offset < self.end
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Range<usize>> for Selection {
|
|
||||||
fn from(value: Range<usize>) -> Self {
|
|
||||||
Self::new(value.start, value.end)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl From<Selection> for Range<usize> {
|
|
||||||
fn from(value: Selection) -> Self {
|
|
||||||
value.start..value.end
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl RangeBounds<usize> for Selection {
|
|
||||||
fn start_bound(&self) -> std::ops::Bound<&usize> {
|
|
||||||
std::ops::Bound::Included(&self.start)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn end_bound(&self) -> std::ops::Bound<&usize> {
|
|
||||||
std::ops::Bound::Excluded(&self.end)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
use std::ops::Range;
|
|
||||||
|
|
||||||
use gpui::{App, Font, Pixels};
|
|
||||||
use ropey::Rope;
|
|
||||||
|
|
||||||
use super::text_wrapper::{LineItem, WrapDisplayPoint};
|
|
||||||
use super::wrap_map::WrapMap;
|
|
||||||
use crate::input::Point as TreeSitterPoint;
|
|
||||||
|
|
||||||
/// DisplayMap is the main interface for Input coordinate mapping.
|
|
||||||
pub struct DisplayMap {
|
|
||||||
wrap_map: WrapMap,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DisplayMap {
|
|
||||||
pub fn new(font: Font, font_size: Pixels, wrap_width: Option<Pixels>) -> Self {
|
|
||||||
Self {
|
|
||||||
wrap_map: WrapMap::new(font, font_size, wrap_width),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get total number of display rows (same as wrap rows without folding)
|
|
||||||
#[inline]
|
|
||||||
pub fn display_row_count(&self) -> usize {
|
|
||||||
self.wrap_map.wrap_row_count()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the buffer line for a given display row
|
|
||||||
pub fn display_row_to_buffer_line(&self, display_row: usize) -> usize {
|
|
||||||
self.wrap_map.wrap_row_to_buffer_line(display_row)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the display row range for a buffer line: [start, end)
|
|
||||||
pub fn buffer_line_to_display_row_range(&self, line: usize) -> Option<Range<usize>> {
|
|
||||||
let range = self.wrap_map.buffer_line_to_wrap_row_range(line);
|
|
||||||
if range.is_empty() { None } else { Some(range) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if a buffer line is completely hidden (never true without folding)
|
|
||||||
#[inline]
|
|
||||||
pub fn is_buffer_line_hidden(&self, _line: usize) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
/// All wrap rows are visible since there's no folding.
|
|
||||||
#[inline]
|
|
||||||
pub fn folded_ranges(&self) -> &[()] {
|
|
||||||
&[]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adjust folds for edit (no-op without folding)
|
|
||||||
pub fn adjust_folds_for_edit(
|
|
||||||
&mut self,
|
|
||||||
_old_text: &Rope,
|
|
||||||
_range: &Range<usize>,
|
|
||||||
_new_text: &str,
|
|
||||||
) {
|
|
||||||
// No-op: no folding
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update text (incremental or full)
|
|
||||||
pub fn on_text_changed(
|
|
||||||
&mut self,
|
|
||||||
changed_text: &Rope,
|
|
||||||
range: &Range<usize>,
|
|
||||||
new_text: &Rope,
|
|
||||||
cx: &mut App,
|
|
||||||
) {
|
|
||||||
self.wrap_map
|
|
||||||
.on_text_changed(changed_text, range, new_text, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update layout parameters (wrap width or font)
|
|
||||||
pub fn on_layout_changed(&mut self, wrap_width: Option<Pixels>, cx: &mut App) {
|
|
||||||
self.wrap_map.on_layout_changed(wrap_width, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set font parameters
|
|
||||||
pub fn set_font(&mut self, font: Font, font_size: Pixels, cx: &mut App) {
|
|
||||||
self.wrap_map.set_font(font, font_size, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Ensure text is prepared (initializes wrapper if needed)
|
|
||||||
pub fn ensure_text_prepared(&mut self, text: &Rope, cx: &mut App) {
|
|
||||||
self.wrap_map.ensure_text_prepared(text, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Initialize with text
|
|
||||||
pub fn set_text(&mut self, text: &Rope, cx: &mut App) {
|
|
||||||
self.wrap_map.set_text(text, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert byte offset to wrap display point (with soft wrap info).
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn offset_to_wrap_display_point(&self, offset: usize) -> WrapDisplayPoint {
|
|
||||||
self.wrap_map.wrapper().offset_to_display_point(offset)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert wrap display point to byte offset.
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn wrap_display_point_to_offset(&self, point: WrapDisplayPoint) -> usize {
|
|
||||||
self.wrap_map.wrapper().display_point_to_offset(point)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert wrap display point to TreeSitterPoint (buffer line/col).
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn wrap_display_point_to_point(&self, point: WrapDisplayPoint) -> TreeSitterPoint {
|
|
||||||
self.wrap_map.wrapper().display_point_to_point(point)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Since there's no folding, wrap row == display row.
|
|
||||||
#[inline]
|
|
||||||
pub fn wrap_row_to_display_row(&self, wrap_row: usize) -> Option<usize> {
|
|
||||||
if wrap_row < self.wrap_row_count() {
|
|
||||||
Some(wrap_row)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Since there's no folding, nearest visible row is the row itself.
|
|
||||||
#[inline]
|
|
||||||
pub fn nearest_visible_display_row(&self, wrap_row: usize) -> usize {
|
|
||||||
wrap_row.min(self.wrap_row_count().saturating_sub(1))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Since there's no folding, display row == wrap row.
|
|
||||||
#[inline]
|
|
||||||
pub fn display_row_to_wrap_row(&self, display_row: usize) -> Option<usize> {
|
|
||||||
if display_row < self.wrap_row_count() {
|
|
||||||
Some(display_row)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the longest row index (by byte length).
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn longest_row(&self) -> usize {
|
|
||||||
self.wrap_map.wrapper().longest_row.row
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get access to line items (for rendering)
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn lines(&self) -> &[LineItem] {
|
|
||||||
self.wrap_map.lines()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the rope text
|
|
||||||
#[inline]
|
|
||||||
pub fn text(&self) -> &Rope {
|
|
||||||
self.wrap_map.text()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calculate how many wrap rows of a buffer line are visible
|
|
||||||
#[inline]
|
|
||||||
pub fn visible_wrap_row_count_for_buffer_line(&self, line: usize) -> usize {
|
|
||||||
self.wrap_map.visible_wrap_row_count_for_buffer_line(line)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the wrap row count
|
|
||||||
#[inline]
|
|
||||||
pub fn wrap_row_count(&self) -> usize {
|
|
||||||
self.wrap_map.wrap_row_count()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the buffer line count (logical lines)
|
|
||||||
#[inline]
|
|
||||||
pub fn buffer_line_count(&self) -> usize {
|
|
||||||
self.wrap_map.buffer_line_count()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
#[allow(clippy::module_inception)]
|
|
||||||
mod display_map;
|
|
||||||
mod text_wrapper;
|
|
||||||
mod wrap_map;
|
|
||||||
|
|
||||||
pub use self::display_map::DisplayMap;
|
|
||||||
pub(crate) use self::text_wrapper::LineLayout;
|
|
||||||
@@ -1,582 +0,0 @@
|
|||||||
use std::ops::Range;
|
|
||||||
|
|
||||||
use gpui::{
|
|
||||||
App, Font, Half, LineFragment, Pixels, Point, ShapedLine, Size, TextAlign, Window, point, px,
|
|
||||||
size,
|
|
||||||
};
|
|
||||||
use ropey::Rope;
|
|
||||||
use smallvec::SmallVec;
|
|
||||||
|
|
||||||
use crate::input::{LastLayout, Point as TreeSitterPoint, RopeExt, WhitespaceIndicators};
|
|
||||||
|
|
||||||
/// A line with soft wrapped lines info.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub(crate) struct LineItem {
|
|
||||||
/// The original line text, without end `\n`.
|
|
||||||
line: Rope,
|
|
||||||
/// The soft wrapped lines relative byte range (0..line.len) of this line (Include first line).
|
|
||||||
///
|
|
||||||
/// Not contains the line end `\n`.
|
|
||||||
pub(crate) wrapped_lines: Vec<Range<usize>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LineItem {
|
|
||||||
/// Get the bytes length of this line.
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn len(&self) -> usize {
|
|
||||||
self.line.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get number of soft wrapped lines of this line (include the first line).
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn lines_len(&self) -> usize {
|
|
||||||
self.wrapped_lines.len()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
pub(crate) struct LongestRow {
|
|
||||||
/// The 0-based row index.
|
|
||||||
pub row: usize,
|
|
||||||
/// The bytes length of the longest line.
|
|
||||||
pub len: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Used to prepare the text with soft wrap to be get lines to displayed in the Editor.
|
|
||||||
///
|
|
||||||
/// After use lines to calculate the scroll size of the Editor.
|
|
||||||
pub(crate) struct TextWrapper {
|
|
||||||
text: Rope,
|
|
||||||
/// Total wrapped lines (Inlucde the first line), value is start and end index of the line.
|
|
||||||
soft_lines: usize,
|
|
||||||
font: Font,
|
|
||||||
font_size: Pixels,
|
|
||||||
/// If is none, it means the text is not wrapped
|
|
||||||
wrap_width: Option<Pixels>,
|
|
||||||
/// The longest (row, bytes len) in characters, used to calculate the horizontal scroll width.
|
|
||||||
pub(crate) longest_row: LongestRow,
|
|
||||||
/// The lines by split \n
|
|
||||||
pub(crate) lines: Vec<LineItem>,
|
|
||||||
|
|
||||||
_initialized: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(unused)]
|
|
||||||
impl TextWrapper {
|
|
||||||
pub(crate) fn new(font: Font, font_size: Pixels, wrap_width: Option<Pixels>) -> Self {
|
|
||||||
Self {
|
|
||||||
text: Rope::new(),
|
|
||||||
font,
|
|
||||||
font_size,
|
|
||||||
wrap_width,
|
|
||||||
soft_lines: 0,
|
|
||||||
longest_row: LongestRow::default(),
|
|
||||||
lines: Vec::new(),
|
|
||||||
_initialized: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn set_default_text(&mut self, text: &Rope) {
|
|
||||||
self.text = text.clone();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get reference to the rope text.
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn text(&self) -> &Rope {
|
|
||||||
&self.text
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the total number of lines including wrapped lines.
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn len(&self) -> usize {
|
|
||||||
self.soft_lines
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the line item by row index.
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn line(&self, row: usize) -> Option<&LineItem> {
|
|
||||||
self.lines.get(row)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn set_wrap_width(&mut self, wrap_width: Option<Pixels>, cx: &mut App) {
|
|
||||||
if wrap_width == self.wrap_width {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.wrap_width = wrap_width;
|
|
||||||
self.update_all(&self.text.clone(), cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn set_font(&mut self, font: Font, font_size: Pixels, cx: &mut App) {
|
|
||||||
if self.font.eq(&font) && self.font_size == font_size {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.font = font;
|
|
||||||
self.font_size = font_size;
|
|
||||||
self.update_all(&self.text.clone(), cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn prepare_if_need(&mut self, text: &Rope, cx: &mut App) -> bool {
|
|
||||||
if self._initialized {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
self._initialized = true;
|
|
||||||
self.update_all(text, cx);
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update the text wrapper and recalculate the wrapped lines.
|
|
||||||
///
|
|
||||||
/// If the `text` is the same as the current text, do nothing.
|
|
||||||
///
|
|
||||||
/// - `changed_text`: The text [`Rope`] that has changed.
|
|
||||||
/// - `range`: The `selected_range` before change.
|
|
||||||
/// - `new_text`: The inserted text.
|
|
||||||
/// - `force`: Whether to force the update, if false, the update will be skipped if the text is the same.
|
|
||||||
/// - `cx`: The application context.
|
|
||||||
pub(crate) fn update(
|
|
||||||
&mut self,
|
|
||||||
changed_text: &Rope,
|
|
||||||
range: &Range<usize>,
|
|
||||||
new_text: &Rope,
|
|
||||||
cx: &mut App,
|
|
||||||
) {
|
|
||||||
let mut line_wrapper = cx
|
|
||||||
.text_system()
|
|
||||||
.line_wrapper(self.font.clone(), self.font_size);
|
|
||||||
self._update(
|
|
||||||
changed_text,
|
|
||||||
range,
|
|
||||||
new_text,
|
|
||||||
&mut |line_str, wrap_width| {
|
|
||||||
line_wrapper
|
|
||||||
.wrap_line(&[LineFragment::text(line_str)], wrap_width)
|
|
||||||
.collect()
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn _update<F>(
|
|
||||||
&mut self,
|
|
||||||
changed_text: &Rope,
|
|
||||||
range: &Range<usize>,
|
|
||||||
new_text: &Rope,
|
|
||||||
wrap_line: &mut F,
|
|
||||||
) where
|
|
||||||
F: FnMut(&str, Pixels) -> Vec<gpui::Boundary>,
|
|
||||||
{
|
|
||||||
// Remove the old changed lines.
|
|
||||||
let start_row = self.text.offset_to_point(range.start).row;
|
|
||||||
let start_row = start_row.min(self.lines.len().saturating_sub(1));
|
|
||||||
let end_row = self.text.offset_to_point(range.end).row;
|
|
||||||
let end_row = end_row.min(self.lines.len().saturating_sub(1));
|
|
||||||
let rows_range = start_row..=end_row;
|
|
||||||
|
|
||||||
if rows_range.contains(&self.longest_row.row) {
|
|
||||||
self.longest_row = LongestRow::default();
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut longest_row_ix = self.longest_row.row;
|
|
||||||
let mut longest_row_len = self.longest_row.len;
|
|
||||||
|
|
||||||
// To add the new lines.
|
|
||||||
let new_start_row = changed_text.offset_to_point(range.start).row;
|
|
||||||
let new_start_offset = changed_text.line_start_offset(new_start_row);
|
|
||||||
let new_end_row = changed_text
|
|
||||||
.offset_to_point(range.start + new_text.len())
|
|
||||||
.row;
|
|
||||||
let new_end_offset = changed_text.line_end_offset(new_end_row);
|
|
||||||
let new_range = new_start_offset..new_end_offset;
|
|
||||||
|
|
||||||
let mut new_lines = vec![];
|
|
||||||
let wrap_width = self.wrap_width;
|
|
||||||
|
|
||||||
// line not contains `\n`.
|
|
||||||
for (ix, line) in Rope::from(changed_text.slice(new_range))
|
|
||||||
.iter_lines()
|
|
||||||
.enumerate()
|
|
||||||
{
|
|
||||||
let line_str = line.to_string();
|
|
||||||
let mut wrapped_lines = vec![];
|
|
||||||
let mut prev_boundary_ix = 0;
|
|
||||||
|
|
||||||
if line_str.len() > longest_row_len {
|
|
||||||
longest_row_ix = new_start_row + ix;
|
|
||||||
longest_row_len = line_str.len();
|
|
||||||
}
|
|
||||||
|
|
||||||
// If wrap_width is Pixels::MAX, skip wrapping to disable word wrap
|
|
||||||
if let Some(wrap_width) = wrap_width {
|
|
||||||
// Here only have wrapped line, if there is no wrap meet, the `line_wraps` result will empty.
|
|
||||||
for boundary in wrap_line(&line_str, wrap_width) {
|
|
||||||
wrapped_lines.push(prev_boundary_ix..boundary.ix);
|
|
||||||
prev_boundary_ix = boundary.ix;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset of the line
|
|
||||||
if !line_str[prev_boundary_ix..].is_empty() || prev_boundary_ix == 0 {
|
|
||||||
wrapped_lines.push(prev_boundary_ix..line.len());
|
|
||||||
}
|
|
||||||
|
|
||||||
new_lines.push(LineItem {
|
|
||||||
line: Rope::from(line),
|
|
||||||
wrapped_lines,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.lines.is_empty() {
|
|
||||||
self.lines = new_lines;
|
|
||||||
} else {
|
|
||||||
self.lines.splice(rows_range, new_lines);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.text = changed_text.clone();
|
|
||||||
self.soft_lines = self.lines.iter().map(|l| l.lines_len()).sum();
|
|
||||||
self.longest_row = LongestRow {
|
|
||||||
row: longest_row_ix,
|
|
||||||
len: longest_row_len,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update the text wrapper and recalculate the wrapped lines.
|
|
||||||
///
|
|
||||||
/// If the `text` is the same as the current text, do nothing.
|
|
||||||
fn update_all(&mut self, text: &Rope, cx: &mut App) {
|
|
||||||
self.update(text, &(0..text.len()), text, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return display point (with soft wrap) from the given byte offset in the text.
|
|
||||||
///
|
|
||||||
/// Panics if the `offset` is out of bounds.
|
|
||||||
pub(crate) fn offset_to_display_point(&self, offset: usize) -> WrapDisplayPoint {
|
|
||||||
let row = self.text.offset_to_point(offset).row;
|
|
||||||
let start = self.text.line_start_offset(row);
|
|
||||||
let line = &self.lines[row];
|
|
||||||
|
|
||||||
let mut wrapped_row = self
|
|
||||||
.lines
|
|
||||||
.iter()
|
|
||||||
.take(row)
|
|
||||||
.map(|l| l.lines_len())
|
|
||||||
.sum::<usize>();
|
|
||||||
|
|
||||||
let local_offset = offset.saturating_sub(start);
|
|
||||||
for (ix, range) in line.wrapped_lines.iter().enumerate() {
|
|
||||||
if range.contains(&local_offset) {
|
|
||||||
return WrapDisplayPoint::new(
|
|
||||||
wrapped_row + ix,
|
|
||||||
ix,
|
|
||||||
local_offset.saturating_sub(range.start),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise return the eof of the line.
|
|
||||||
let last_range = line.wrapped_lines.last().unwrap_or(&(0..0));
|
|
||||||
let ix = line.lines_len().saturating_sub(1);
|
|
||||||
|
|
||||||
WrapDisplayPoint::new(wrapped_row + ix, ix, last_range.len())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return byte offset in the text from the given display point (with soft wrap).
|
|
||||||
///
|
|
||||||
/// Panics if the `point.row` is out of bounds.
|
|
||||||
pub(crate) fn display_point_to_offset(&self, point: WrapDisplayPoint) -> usize {
|
|
||||||
let mut wrapped_row = 0;
|
|
||||||
for (row, line) in self.lines.iter().enumerate() {
|
|
||||||
if wrapped_row + line.lines_len() > point.row {
|
|
||||||
let line_start = self.text.line_start_offset(row);
|
|
||||||
let local_row = point.row.saturating_sub(wrapped_row);
|
|
||||||
if let Some(range) = line.wrapped_lines.get(local_row) {
|
|
||||||
return line_start + (range.start + point.column).min(range.end);
|
|
||||||
} else {
|
|
||||||
// If not found, return the end of the line.
|
|
||||||
return line_start + line.len();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
wrapped_row += line.lines_len();
|
|
||||||
}
|
|
||||||
|
|
||||||
self.text.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn display_point_to_point(&self, point: WrapDisplayPoint) -> TreeSitterPoint {
|
|
||||||
let offset = self.display_point_to_offset(point);
|
|
||||||
self.text.offset_to_point(offset)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn point_to_display_point(&self, point: TreeSitterPoint) -> WrapDisplayPoint {
|
|
||||||
let offset = self.text.point_to_offset(point);
|
|
||||||
self.offset_to_display_point(offset)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A display point within the soft-wrapped text.
|
|
||||||
///
|
|
||||||
/// This represents a position in the text after soft-wrapping,
|
|
||||||
/// with an additional `local_row` field tracking the wrap line
|
|
||||||
/// within the original buffer line.
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub(crate) struct WrapDisplayPoint {
|
|
||||||
/// The 0-based soft wrapped row index in the text.
|
|
||||||
pub row: usize,
|
|
||||||
/// The 0-based row index in local line (include first line).
|
|
||||||
///
|
|
||||||
/// This value only valid when return from [`TextWrapper::offset_to_display_point`], otherwise it will be ignored.
|
|
||||||
pub local_row: usize,
|
|
||||||
/// The 0-based column byte index in the display line (with soft wrap).
|
|
||||||
pub column: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WrapDisplayPoint {
|
|
||||||
pub fn new(row: usize, local_row: usize, column: usize) -> Self {
|
|
||||||
Self {
|
|
||||||
row,
|
|
||||||
local_row,
|
|
||||||
column,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The layout info of a line with soft wrapped lines.
|
|
||||||
pub(crate) struct LineLayout {
|
|
||||||
/// Total bytes length of this line.
|
|
||||||
len: usize,
|
|
||||||
/// The soft wrapped lines of this line (Include the first line).
|
|
||||||
pub(crate) wrapped_lines: SmallVec<[ShapedLine; 1]>,
|
|
||||||
pub(crate) longest_width: Pixels,
|
|
||||||
pub(crate) whitespace_indicators: Option<WhitespaceIndicators>,
|
|
||||||
/// Whitespace indicators: (line_index, x_position, is_tab)
|
|
||||||
pub(crate) whitespace_chars: Vec<(usize, Pixels, bool)>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LineLayout {
|
|
||||||
pub(crate) fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
len: 0,
|
|
||||||
longest_width: px(0.),
|
|
||||||
wrapped_lines: SmallVec::new(),
|
|
||||||
whitespace_chars: Vec::new(),
|
|
||||||
whitespace_indicators: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn lines(mut self, wrapped_lines: SmallVec<[ShapedLine; 1]>) -> Self {
|
|
||||||
self.set_wrapped_lines(wrapped_lines);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn set_wrapped_lines(&mut self, wrapped_lines: SmallVec<[ShapedLine; 1]>) {
|
|
||||||
self.len = wrapped_lines.iter().map(|l| l.len).sum();
|
|
||||||
let width = wrapped_lines
|
|
||||||
.iter()
|
|
||||||
.map(|l| l.width)
|
|
||||||
.max()
|
|
||||||
.unwrap_or_default();
|
|
||||||
self.longest_width = width;
|
|
||||||
self.wrapped_lines = wrapped_lines;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn with_whitespaces(mut self, indicators: Option<WhitespaceIndicators>) -> Self {
|
|
||||||
self.whitespace_indicators = indicators;
|
|
||||||
let Some(indicators) = self.whitespace_indicators.as_ref() else {
|
|
||||||
return self;
|
|
||||||
};
|
|
||||||
|
|
||||||
let space_indicator_offset = indicators.space.width.half();
|
|
||||||
|
|
||||||
for (line_index, wrapped_line) in self.wrapped_lines.iter().enumerate() {
|
|
||||||
for (relative_offset, c) in wrapped_line.text.char_indices() {
|
|
||||||
if matches!(c, ' ' | '\t') {
|
|
||||||
let is_tab = c == '\t';
|
|
||||||
let start_x = wrapped_line.x_for_index(relative_offset);
|
|
||||||
let end_x = wrapped_line.x_for_index(relative_offset + c.len_utf8());
|
|
||||||
// Center the indicator in the actual character's space
|
|
||||||
let x_position = if c == ' ' {
|
|
||||||
(start_x + end_x).half() - space_indicator_offset
|
|
||||||
} else {
|
|
||||||
start_x
|
|
||||||
};
|
|
||||||
|
|
||||||
self.whitespace_chars.push((line_index, x_position, is_tab));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn len(&self) -> usize {
|
|
||||||
self.len
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the position (x, y) for the given index in this line layout.
|
|
||||||
///
|
|
||||||
/// - The `offset` is a local byte index in this line layout.
|
|
||||||
/// - When `line_end_affinity` is true, an offset at a soft wrap boundary is placed at
|
|
||||||
/// the end of the current visual line rather than the start of the next one.
|
|
||||||
/// - The return value is relative to the top-left corner of this line layout, start from (0, 0)
|
|
||||||
pub(crate) fn position_for_index(
|
|
||||||
&self,
|
|
||||||
offset: usize,
|
|
||||||
last_layout: &LastLayout,
|
|
||||||
line_end_affinity: bool,
|
|
||||||
) -> Option<Point<Pixels>> {
|
|
||||||
let mut acc_len = 0;
|
|
||||||
let mut offset_y = px(0.);
|
|
||||||
|
|
||||||
let x_offset = last_layout.alignment_offset(self.longest_width);
|
|
||||||
|
|
||||||
for (i, line) in self.wrapped_lines.iter().enumerate() {
|
|
||||||
let is_last = i + 1 == self.wrapped_lines.len();
|
|
||||||
|
|
||||||
let matches = if line.len == 0 {
|
|
||||||
// Empty visual lines still own their boundary offset.
|
|
||||||
offset == acc_len
|
|
||||||
} else if is_last || line_end_affinity {
|
|
||||||
// Inclusive: cursor can sit at end of this visual line.
|
|
||||||
offset >= acc_len && offset <= acc_len + line.len
|
|
||||||
} else {
|
|
||||||
// Exclusive: boundary offset belongs to the next visual line.
|
|
||||||
offset >= acc_len && offset < acc_len + line.len
|
|
||||||
};
|
|
||||||
|
|
||||||
if matches {
|
|
||||||
let x = line.x_for_index(offset.saturating_sub(acc_len)) + x_offset;
|
|
||||||
return Some(point(x, offset_y));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always advance by actual line length. The last line gets +1 so the
|
|
||||||
// cursor can be placed after the final character.
|
|
||||||
acc_len += if is_last { line.len + 1 } else { line.len };
|
|
||||||
offset_y += last_layout.line_height;
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the closest index for the given x in this line layout.
|
|
||||||
pub(crate) fn closest_index_for_x(&self, x: Pixels, last_layout: &LastLayout) -> usize {
|
|
||||||
let mut acc_len = 0;
|
|
||||||
let x_offset = last_layout.alignment_offset(self.longest_width);
|
|
||||||
let x = x - x_offset;
|
|
||||||
|
|
||||||
for (i, line) in self.wrapped_lines.iter().enumerate() {
|
|
||||||
let is_last = i + 1 == self.wrapped_lines.len();
|
|
||||||
if x <= line.width {
|
|
||||||
let mut ix = line.closest_index_for_x(x);
|
|
||||||
if !is_last && ix == line.text.len() {
|
|
||||||
// For soft wrap line, we can't put the cursor at the end of the line.
|
|
||||||
let c_len = line.text.chars().last().map(|c| c.len_utf8()).unwrap_or(0);
|
|
||||||
ix = ix.saturating_sub(c_len);
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc_len + ix;
|
|
||||||
}
|
|
||||||
acc_len += line.text.len();
|
|
||||||
}
|
|
||||||
|
|
||||||
acc_len
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the index for the given position (x, y) in this line layout.
|
|
||||||
///
|
|
||||||
/// The `pos` is relative to the top-left corner of this line layout, start from (0, 0)
|
|
||||||
/// The return value is a local byte index in this line layout, start from 0.
|
|
||||||
pub(crate) fn closest_index_for_position(
|
|
||||||
&self,
|
|
||||||
pos: Point<Pixels>,
|
|
||||||
last_layout: &LastLayout,
|
|
||||||
) -> Option<usize> {
|
|
||||||
let mut offset = 0;
|
|
||||||
let mut line_top = px(0.);
|
|
||||||
let x_offset = last_layout.alignment_offset(self.longest_width);
|
|
||||||
for (i, line) in self.wrapped_lines.iter().enumerate() {
|
|
||||||
let is_last = i + 1 == self.wrapped_lines.len();
|
|
||||||
let line_bottom = line_top + last_layout.line_height;
|
|
||||||
if pos.y >= line_top && pos.y < line_bottom {
|
|
||||||
let mut ix = line.closest_index_for_x(pos.x - x_offset);
|
|
||||||
if !is_last && ix == line.text.len() {
|
|
||||||
// For soft wrap line, we can't put the cursor at the end of the line.
|
|
||||||
let c_len = line.text.chars().last().map(|c| c.len_utf8()).unwrap_or(0);
|
|
||||||
ix = ix.saturating_sub(c_len);
|
|
||||||
}
|
|
||||||
return Some(offset + ix);
|
|
||||||
}
|
|
||||||
|
|
||||||
offset += line.text.len();
|
|
||||||
line_top = line_bottom;
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn index_for_position(
|
|
||||||
&self,
|
|
||||||
pos: Point<Pixels>,
|
|
||||||
last_layout: &LastLayout,
|
|
||||||
) -> Option<usize> {
|
|
||||||
let mut offset = 0;
|
|
||||||
let mut line_top = px(0.);
|
|
||||||
let x_offset = last_layout.alignment_offset(self.longest_width);
|
|
||||||
for line in self.wrapped_lines.iter() {
|
|
||||||
let line_bottom = line_top + last_layout.line_height;
|
|
||||||
if pos.y >= line_top && pos.y < line_bottom {
|
|
||||||
let ix = line.index_for_x(pos.x - x_offset)?;
|
|
||||||
return Some(offset + ix);
|
|
||||||
}
|
|
||||||
|
|
||||||
offset += line.text.len();
|
|
||||||
line_top = line_bottom;
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn size(&self, line_height: Pixels) -> Size<Pixels> {
|
|
||||||
size(self.longest_width, self.wrapped_lines.len() * line_height)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn paint(
|
|
||||||
&self,
|
|
||||||
pos: Point<Pixels>,
|
|
||||||
line_height: Pixels,
|
|
||||||
text_align: TextAlign,
|
|
||||||
align_width: Option<Pixels>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut App,
|
|
||||||
) {
|
|
||||||
for (ix, line) in self.wrapped_lines.iter().enumerate() {
|
|
||||||
_ = line.paint(
|
|
||||||
pos + point(px(0.), ix * line_height),
|
|
||||||
line_height,
|
|
||||||
text_align,
|
|
||||||
align_width,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Paint whitespace indicators
|
|
||||||
if let Some(indicators) = self.whitespace_indicators.as_ref() {
|
|
||||||
for (line_index, x_position, is_tab) in &self.whitespace_chars {
|
|
||||||
let invisible = if *is_tab {
|
|
||||||
indicators.tab.clone()
|
|
||||||
} else {
|
|
||||||
indicators.space.clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
let origin = point(
|
|
||||||
pos.x + *x_position,
|
|
||||||
pos.y + *line_index as f32 * line_height,
|
|
||||||
);
|
|
||||||
|
|
||||||
_ = invisible.paint(origin, line_height, text_align, align_width, window, cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
/// WrapMap: Soft-wrapping layer (Buffer → Wrap rows).
|
|
||||||
///
|
|
||||||
/// This module wraps the existing TextWrapper and provides:
|
|
||||||
/// - BufferPoint ↔ WrapPoint mapping
|
|
||||||
/// - Efficient buffer_line → wrap_row queries via prefix sum cache
|
|
||||||
/// - Incremental updates when text or layout changes
|
|
||||||
use std::ops::Range;
|
|
||||||
|
|
||||||
use gpui::{App, Font, Pixels};
|
|
||||||
use ropey::Rope;
|
|
||||||
|
|
||||||
use super::text_wrapper::{LineItem, TextWrapper};
|
|
||||||
|
|
||||||
/// WrapMap manages soft-wrapping and provides buffer ↔ wrap coordinate mapping.
|
|
||||||
pub struct WrapMap {
|
|
||||||
/// The underlying text wrapper (reuses existing implementation)
|
|
||||||
wrapper: TextWrapper,
|
|
||||||
|
|
||||||
/// Prefix sum cache: buffer_line_starts[line] = first wrap_row for buffer line `line`
|
|
||||||
/// This allows O(1) lookup of buffer_line → wrap_row
|
|
||||||
buffer_line_starts: Vec<usize>,
|
|
||||||
|
|
||||||
/// Cached line count from last rebuild
|
|
||||||
cached_line_count: usize,
|
|
||||||
|
|
||||||
/// Cached total wrap row count from last rebuild.
|
|
||||||
/// Used together with `cached_line_count` to detect if the cache is stale.
|
|
||||||
/// When soft wrap changes a line's wrap count without changing buffer line count,
|
|
||||||
/// this catches the staleness.
|
|
||||||
cached_wrap_row_count: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WrapMap {
|
|
||||||
pub fn new(font: Font, font_size: Pixels, wrap_width: Option<Pixels>) -> Self {
|
|
||||||
Self {
|
|
||||||
wrapper: TextWrapper::new(font, font_size, wrap_width),
|
|
||||||
buffer_line_starts: Vec::new(),
|
|
||||||
cached_line_count: 0,
|
|
||||||
cached_wrap_row_count: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get total number of wrap rows (visual rows after soft-wrapping)
|
|
||||||
#[inline]
|
|
||||||
pub fn wrap_row_count(&self) -> usize {
|
|
||||||
self.wrapper.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get total number of buffer lines (logical lines)
|
|
||||||
#[inline]
|
|
||||||
pub fn buffer_line_count(&self) -> usize {
|
|
||||||
self.wrapper.lines.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the buffer line for a given wrap row
|
|
||||||
pub fn wrap_row_to_buffer_line(&self, wrap_row: usize) -> usize {
|
|
||||||
if wrap_row >= self.wrap_row_count() {
|
|
||||||
return self.buffer_line_count().saturating_sub(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Binary search in prefix sum cache
|
|
||||||
match self.buffer_line_starts.binary_search(&wrap_row) {
|
|
||||||
Ok(line) => line,
|
|
||||||
Err(insert_pos) => insert_pos.saturating_sub(1),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the first wrap row for a given buffer line
|
|
||||||
pub fn buffer_line_to_first_wrap_row(&self, line: usize) -> usize {
|
|
||||||
if line >= self.buffer_line_starts.len() {
|
|
||||||
return self.wrap_row_count();
|
|
||||||
}
|
|
||||||
self.buffer_line_starts[line]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the wrap row range for a buffer line: [start, end)
|
|
||||||
pub fn buffer_line_to_wrap_row_range(&self, line: usize) -> Range<usize> {
|
|
||||||
let start = self.buffer_line_to_first_wrap_row(line);
|
|
||||||
let end = if line + 1 < self.buffer_line_starts.len() {
|
|
||||||
self.buffer_line_starts[line + 1]
|
|
||||||
} else {
|
|
||||||
self.wrap_row_count()
|
|
||||||
};
|
|
||||||
start..end
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update text (incremental or full)
|
|
||||||
pub fn on_text_changed(
|
|
||||||
&mut self,
|
|
||||||
changed_text: &Rope,
|
|
||||||
range: &Range<usize>,
|
|
||||||
new_text: &Rope,
|
|
||||||
cx: &mut App,
|
|
||||||
) {
|
|
||||||
self.wrapper.update(changed_text, range, new_text, cx);
|
|
||||||
self.rebuild_cache();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update layout parameters (wrap width or font)
|
|
||||||
pub fn on_layout_changed(&mut self, wrap_width: Option<Pixels>, cx: &mut App) {
|
|
||||||
self.wrapper.set_wrap_width(wrap_width, cx);
|
|
||||||
self.rebuild_cache();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set font parameters
|
|
||||||
pub fn set_font(&mut self, font: Font, font_size: Pixels, cx: &mut App) {
|
|
||||||
self.wrapper.set_font(font, font_size, cx);
|
|
||||||
self.rebuild_cache();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Ensure text is prepared (initializes wrapper if needed)
|
|
||||||
pub fn ensure_text_prepared(&mut self, text: &Rope, cx: &mut App) -> bool {
|
|
||||||
let did_initialize = self.wrapper.prepare_if_need(text, cx);
|
|
||||||
if did_initialize {
|
|
||||||
self.rebuild_cache();
|
|
||||||
}
|
|
||||||
did_initialize
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Initialize with text
|
|
||||||
pub fn set_text(&mut self, text: &Rope, cx: &mut App) {
|
|
||||||
self.wrapper.set_default_text(text);
|
|
||||||
self.wrapper.prepare_if_need(text, cx);
|
|
||||||
self.rebuild_cache();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Rebuild the prefix sum cache: buffer_line_starts
|
|
||||||
fn rebuild_cache(&mut self) {
|
|
||||||
let line_count = self.wrapper.lines.len();
|
|
||||||
let wrap_row_count = self.wrapper.len();
|
|
||||||
|
|
||||||
// Skip if nothing changed: both buffer line count and total wrap row count must match.
|
|
||||||
if line_count == self.cached_line_count
|
|
||||||
&& wrap_row_count == self.cached_wrap_row_count
|
|
||||||
&& !self.buffer_line_starts.is_empty()
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.buffer_line_starts.clear();
|
|
||||||
|
|
||||||
let mut wrap_row = 0;
|
|
||||||
for line_item in &self.wrapper.lines {
|
|
||||||
self.buffer_line_starts.push(wrap_row);
|
|
||||||
wrap_row += line_item.lines_len();
|
|
||||||
}
|
|
||||||
|
|
||||||
self.cached_line_count = line_count;
|
|
||||||
self.cached_wrap_row_count = wrap_row_count;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get access to the underlying wrapper (for rendering/hit-testing)
|
|
||||||
pub(crate) fn wrapper(&self) -> &TextWrapper {
|
|
||||||
&self.wrapper
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get access to line items (for rendering)
|
|
||||||
pub(crate) fn lines(&self) -> &[LineItem] {
|
|
||||||
&self.wrapper.lines
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the rope text
|
|
||||||
pub fn text(&self) -> &Rope {
|
|
||||||
self.wrapper.text()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calculate how many wrap rows of a buffer line are visible.
|
|
||||||
/// Without folding, all wrap rows are visible.
|
|
||||||
pub fn visible_wrap_row_count_for_buffer_line(&self, line: usize) -> usize {
|
|
||||||
self.buffer_line_to_wrap_row_range(line).len()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,269 +0,0 @@
|
|||||||
use gpui::{Context, EntityInputHandler, SharedString, Window};
|
|
||||||
use ropey::RopeSlice;
|
|
||||||
|
|
||||||
use crate::input::mode::InputMode;
|
|
||||||
use crate::input::{Indent, IndentInline, InputState, Outdent, OutdentInline};
|
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
|
||||||
pub struct TabSize {
|
|
||||||
/// Default is 2
|
|
||||||
pub tab_size: usize,
|
|
||||||
/// Set true to use `\t` as tab indent, default is false
|
|
||||||
pub hard_tabs: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for TabSize {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
tab_size: 2,
|
|
||||||
hard_tabs: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TabSize {
|
|
||||||
pub(super) fn to_string(self) -> SharedString {
|
|
||||||
if self.hard_tabs {
|
|
||||||
"\t".into()
|
|
||||||
} else {
|
|
||||||
" ".repeat(self.tab_size).into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Count the indent size of the line in spaces.
|
|
||||||
pub fn indent_count(&self, line: &RopeSlice) -> usize {
|
|
||||||
let mut count = 0;
|
|
||||||
for ch in line.chars() {
|
|
||||||
match ch {
|
|
||||||
'\t' => count += self.tab_size,
|
|
||||||
' ' => count += 1,
|
|
||||||
_ => break,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
count
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl InputState {
|
|
||||||
/// Set the tab size for the input.
|
|
||||||
///
|
|
||||||
/// Only for [`InputMode::PlainText`] mode with multi_line.
|
|
||||||
pub fn tab_size(mut self, tab: TabSize) -> Self {
|
|
||||||
debug_assert!(self.mode.is_multi_line());
|
|
||||||
if let InputMode::PlainText { tab: t, .. } = &mut self.mode {
|
|
||||||
*t = tab;
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn indent_inline(
|
|
||||||
&mut self,
|
|
||||||
_: &IndentInline,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
self.indent(false, window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn indent_block(&mut self, _: &Indent, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
self.indent(true, window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn outdent_inline(
|
|
||||||
&mut self,
|
|
||||||
_: &OutdentInline,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
self.outdent(false, window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn outdent_block(
|
|
||||||
&mut self,
|
|
||||||
_: &Outdent,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
self.outdent(true, window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn indent(&mut self, block: bool, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
if !self.mode.is_indentable() {
|
|
||||||
cx.propagate();
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let tab_indent = self.mode.tab_size().to_string();
|
|
||||||
let selected_range = self.selected_range;
|
|
||||||
let mut added_len = 0;
|
|
||||||
let is_selected = !self.selected_range.is_empty();
|
|
||||||
|
|
||||||
if is_selected || block {
|
|
||||||
let start_offset = self.start_of_line_of_selection(window, cx);
|
|
||||||
let mut offset = start_offset;
|
|
||||||
|
|
||||||
let selected_text = self
|
|
||||||
.text_for_range(
|
|
||||||
self.range_to_utf16(&(offset..selected_range.end)),
|
|
||||||
&mut None,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.unwrap_or("".into());
|
|
||||||
|
|
||||||
for line in selected_text.split('\n') {
|
|
||||||
self.replace_text_in_range_silent(
|
|
||||||
Some(self.range_to_utf16(&(offset..offset))),
|
|
||||||
&tab_indent,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
added_len += tab_indent.len();
|
|
||||||
// +1 for "\n", the `\r` is included in the `line`.
|
|
||||||
offset += line.len() + tab_indent.len() + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if is_selected {
|
|
||||||
self.selected_range = (start_offset..selected_range.end + added_len).into();
|
|
||||||
} else {
|
|
||||||
self.selected_range =
|
|
||||||
(selected_range.start + added_len..selected_range.end + added_len).into();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Selected none
|
|
||||||
let offset = self.selected_range.start;
|
|
||||||
self.replace_text_in_range_silent(
|
|
||||||
Some(self.range_to_utf16(&(offset..offset))),
|
|
||||||
&tab_indent,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
added_len = tab_indent.len();
|
|
||||||
|
|
||||||
self.selected_range =
|
|
||||||
(selected_range.start + added_len..selected_range.end + added_len).into();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn outdent(&mut self, block: bool, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
if !self.mode.is_indentable() {
|
|
||||||
cx.propagate();
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let tab_indent = self.mode.tab_size().to_string();
|
|
||||||
let selected_range = self.selected_range;
|
|
||||||
let mut removed_len = 0;
|
|
||||||
let is_selected = !self.selected_range.is_empty();
|
|
||||||
|
|
||||||
if is_selected || block {
|
|
||||||
let start_offset = self.start_of_line_of_selection(window, cx);
|
|
||||||
let mut offset = start_offset;
|
|
||||||
|
|
||||||
let selected_text = self
|
|
||||||
.text_for_range(
|
|
||||||
self.range_to_utf16(&(offset..selected_range.end)),
|
|
||||||
&mut None,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.unwrap_or("".into());
|
|
||||||
|
|
||||||
for line in selected_text.split('\n') {
|
|
||||||
if line.starts_with(tab_indent.as_ref()) {
|
|
||||||
self.replace_text_in_range_silent(
|
|
||||||
Some(self.range_to_utf16(&(offset..offset + tab_indent.len()))),
|
|
||||||
"",
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
removed_len += tab_indent.len();
|
|
||||||
|
|
||||||
// +1 for "\n"
|
|
||||||
offset += line.len().saturating_sub(tab_indent.len()) + 1;
|
|
||||||
} else {
|
|
||||||
offset += line.len() + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if is_selected {
|
|
||||||
self.selected_range =
|
|
||||||
(start_offset..selected_range.end.saturating_sub(removed_len)).into();
|
|
||||||
} else {
|
|
||||||
self.selected_range = (selected_range.start.saturating_sub(removed_len)
|
|
||||||
..selected_range.end.saturating_sub(removed_len))
|
|
||||||
.into();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Selected none
|
|
||||||
let start_offset = self.selected_range.start;
|
|
||||||
let offset = self.start_of_line_of_selection(window, cx);
|
|
||||||
let offset = self.offset_from_utf16(self.offset_to_utf16(offset));
|
|
||||||
// FIXME: To improve performance
|
|
||||||
if self
|
|
||||||
.text
|
|
||||||
.slice(offset..self.text.len())
|
|
||||||
.to_string()
|
|
||||||
.starts_with(tab_indent.as_ref())
|
|
||||||
{
|
|
||||||
self.replace_text_in_range_silent(
|
|
||||||
Some(self.range_to_utf16(&(offset..offset + tab_indent.len()))),
|
|
||||||
"",
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
removed_len = tab_indent.len();
|
|
||||||
let new_offset = start_offset.saturating_sub(removed_len);
|
|
||||||
self.selected_range = (new_offset..new_offset).into();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use ropey::RopeSlice;
|
|
||||||
|
|
||||||
use super::TabSize;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_tab_size() {
|
|
||||||
let tab = TabSize {
|
|
||||||
tab_size: 2,
|
|
||||||
hard_tabs: false,
|
|
||||||
};
|
|
||||||
assert_eq!(tab.to_string(), " ");
|
|
||||||
let tab = TabSize {
|
|
||||||
tab_size: 4,
|
|
||||||
hard_tabs: false,
|
|
||||||
};
|
|
||||||
assert_eq!(tab.to_string(), " ");
|
|
||||||
|
|
||||||
let tab = TabSize {
|
|
||||||
tab_size: 2,
|
|
||||||
hard_tabs: true,
|
|
||||||
};
|
|
||||||
assert_eq!(tab.to_string(), "\t");
|
|
||||||
let tab = TabSize {
|
|
||||||
tab_size: 4,
|
|
||||||
hard_tabs: true,
|
|
||||||
};
|
|
||||||
assert_eq!(tab.to_string(), "\t");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_tab_size_indent_count() {
|
|
||||||
let tab = TabSize {
|
|
||||||
tab_size: 4,
|
|
||||||
hard_tabs: false,
|
|
||||||
};
|
|
||||||
assert_eq!(tab.indent_count(&RopeSlice::from("abc")), 0);
|
|
||||||
assert_eq!(tab.indent_count(&RopeSlice::from(" abc")), 2);
|
|
||||||
assert_eq!(tab.indent_count(&RopeSlice::from(" abc")), 4);
|
|
||||||
assert_eq!(tab.indent_count(&RopeSlice::from("\tabc")), 4);
|
|
||||||
assert_eq!(tab.indent_count(&RopeSlice::from(" \tabc")), 6);
|
|
||||||
assert_eq!(tab.indent_count(&RopeSlice::from(" \t abc ")), 6);
|
|
||||||
assert_eq!(tab.indent_count(&RopeSlice::from("abc")), 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+112
-169
@@ -1,31 +1,76 @@
|
|||||||
use gpui::prelude::FluentBuilder as _;
|
use gpui::prelude::FluentBuilder as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, DefiniteLength, Edges, EdgesRefinement, Entity, Hsla, InteractiveElement as _,
|
AnyElement, App, DefiniteLength, Edges, Entity, Hsla, InteractiveElement as _, IntoElement,
|
||||||
IntoElement, MouseButton, ParentElement as _, Rems, RenderOnce, StyleRefinement, Styled,
|
MouseButton, ParentElement as _, Pixels, Rems, RenderOnce, StyleRefinement, Styled, TextAlign,
|
||||||
TextAlign, Window, div, px, relative,
|
Window, div, px, relative,
|
||||||
};
|
};
|
||||||
|
use gpui_base::InputBase;
|
||||||
|
use gpui_base::input::{InputBaseState, InputEditorStyle, InputMode, InputModeKind, TextareaMode};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
|
|
||||||
use super::InputState;
|
|
||||||
use super::element::EditorScrollbar;
|
|
||||||
use crate::button::{Button, ButtonVariants as _};
|
use crate::button::{Button, ButtonVariants as _};
|
||||||
use crate::indicator::Indicator;
|
use crate::indicator::Indicator;
|
||||||
use crate::input::clear_button;
|
use crate::input::clear_button;
|
||||||
use crate::{IconName, Selectable, Sizable, Size, StyleSized, StyledExt, h_flex, v_flex};
|
use crate::{IconName, Selectable, Sizable, Size, StyleSized, StyledExt, h_flex, v_flex};
|
||||||
|
|
||||||
/// Returns `(background, foreground)` colors for input-like components.
|
/// The background of an input frame, which reads muted while the input is disabled.
|
||||||
pub(crate) fn input_style(disabled: bool, cx: &App) -> (Hsla, Hsla) {
|
fn input_background(disabled: bool, cx: &App) -> Hsla {
|
||||||
if disabled {
|
if disabled {
|
||||||
(cx.theme().surface_background, cx.theme().text_muted)
|
cx.theme().surface_background
|
||||||
} else {
|
} else {
|
||||||
(cx.theme().elevated_surface_background, cx.theme().text)
|
cx.theme().elevated_surface_background
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A text input element bind to an [`InputState`].
|
/// The colors base paints input text with, read from the coop theme.
|
||||||
|
///
|
||||||
|
/// Base fills in any color left transparent from its own palette, and that
|
||||||
|
/// palette is only a projection of this one, so every color coop paints with is
|
||||||
|
/// named here rather than left to resolve.
|
||||||
|
fn input_editor_style(cx: &App) -> InputEditorStyle {
|
||||||
|
let theme = cx.theme();
|
||||||
|
InputEditorStyle {
|
||||||
|
foreground: theme.text,
|
||||||
|
muted_foreground: theme.text_muted,
|
||||||
|
background: theme.elevated_surface_background,
|
||||||
|
border: theme.border,
|
||||||
|
selection: theme.selection,
|
||||||
|
caret: theme.cursor,
|
||||||
|
..InputEditorStyle::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The input's own padding, resolved to pixels.
|
||||||
|
///
|
||||||
|
/// Base applies the multi-line padding itself so that the text, the gutter, and
|
||||||
|
/// the scrollbar share one inset, and the single-line frame carries its own.
|
||||||
|
/// Both come from the same size table, resolved through the window's rem size.
|
||||||
|
fn input_paddings(size: Size, style: &StyleRefinement, window: &Window) -> Edges<Pixels> {
|
||||||
|
let mut probe = div().input_px(size).input_py(size).refine_style(style);
|
||||||
|
let padding = probe.style().padding.clone();
|
||||||
|
let base_size = window.text_style().font_size;
|
||||||
|
let rem_size = window.rem_size();
|
||||||
|
let resolve = |value: Option<DefiniteLength>| {
|
||||||
|
value
|
||||||
|
.map(|value| value.to_pixels(base_size, rem_size))
|
||||||
|
.unwrap_or(px(0.))
|
||||||
|
};
|
||||||
|
|
||||||
|
Edges {
|
||||||
|
left: resolve(padding.left),
|
||||||
|
right: resolve(padding.right),
|
||||||
|
top: resolve(padding.top),
|
||||||
|
bottom: resolve(padding.bottom),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A text input element bound to an [`InputState`] or a [`TextareaState`].
|
||||||
|
///
|
||||||
|
/// The editing kind lives on the state, so `Input::new` accepts either and
|
||||||
|
/// infers which one is rendered.
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct Input {
|
pub struct Input<M: InputModeKind = InputMode> {
|
||||||
state: Entity<InputState>,
|
state: Entity<InputBaseState<M>>,
|
||||||
style: StyleRefinement,
|
style: StyleRefinement,
|
||||||
size: Size,
|
size: Size,
|
||||||
prefix: Option<AnyElement>,
|
prefix: Option<AnyElement>,
|
||||||
@@ -39,14 +84,17 @@ pub struct Input {
|
|||||||
selected: bool,
|
selected: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Sizable for Input {
|
/// A styled multi-line text input.
|
||||||
|
pub type Textarea = Input<TextareaMode>;
|
||||||
|
|
||||||
|
impl<M: InputModeKind> Sizable for Input<M> {
|
||||||
fn with_size(mut self, size: impl Into<Size>) -> Self {
|
fn with_size(mut self, size: impl Into<Size>) -> Self {
|
||||||
self.size = size.into();
|
self.size = size.into();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Selectable for Input {
|
impl<M: InputModeKind> Selectable for Input<M> {
|
||||||
fn selected(mut self, selected: bool) -> Self {
|
fn selected(mut self, selected: bool) -> Self {
|
||||||
self.selected = selected;
|
self.selected = selected;
|
||||||
self
|
self
|
||||||
@@ -57,9 +105,9 @@ impl Selectable for Input {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Input {
|
impl<M: InputModeKind> Input<M> {
|
||||||
/// Create a new [`Input`] element bind to the [`InputState`].
|
/// Create a new [`Input`] element bind to the given state.
|
||||||
pub fn new(state: &Entity<InputState>) -> Self {
|
pub fn new(state: &Entity<InputBaseState<M>>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
state: state.clone(),
|
state: state.clone(),
|
||||||
size: Size::default(),
|
size: Size::default(),
|
||||||
@@ -128,8 +176,7 @@ impl Input {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_toggle_mask_button(state: &Entity<InputState>, cx: &App) -> impl IntoElement {
|
fn render_toggle_mask_button(state: &Entity<InputBaseState<M>>) -> impl IntoElement {
|
||||||
let _masked = state.read(cx).masked;
|
|
||||||
Button::new("toggle-mask")
|
Button::new("toggle-mask")
|
||||||
.icon(IconName::Eye)
|
.icon(IconName::Eye)
|
||||||
.xsmall()
|
.xsmall()
|
||||||
@@ -137,78 +184,42 @@ impl Input {
|
|||||||
.tab_stop(false)
|
.tab_stop(false)
|
||||||
.on_click({
|
.on_click({
|
||||||
let state = state.clone();
|
let state = state.clone();
|
||||||
move |_, window, cx| {
|
move |_, window, cx| state.update(cx, |state, cx| state.toggle_masked(window, cx))
|
||||||
state.update(cx, |state, cx| {
|
|
||||||
state.set_masked(!state.masked, window, cx);
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This method must after the refine_style.
|
|
||||||
fn render_editor(
|
|
||||||
paddings: EdgesRefinement<DefiniteLength>,
|
|
||||||
input_state: &Entity<InputState>,
|
|
||||||
state: &InputState,
|
|
||||||
window: &Window,
|
|
||||||
) -> impl IntoElement {
|
|
||||||
let base_size = window.text_style().font_size;
|
|
||||||
let rem_size = window.rem_size();
|
|
||||||
|
|
||||||
let paddings = Edges {
|
|
||||||
left: paddings
|
|
||||||
.left
|
|
||||||
.map(|v| v.to_pixels(base_size, rem_size))
|
|
||||||
.unwrap_or(px(0.)),
|
|
||||||
right: paddings
|
|
||||||
.right
|
|
||||||
.map(|v| v.to_pixels(base_size, rem_size))
|
|
||||||
.unwrap_or(px(0.)),
|
|
||||||
top: paddings
|
|
||||||
.top
|
|
||||||
.map(|v| v.to_pixels(base_size, rem_size))
|
|
||||||
.unwrap_or(px(0.)),
|
|
||||||
bottom: paddings
|
|
||||||
.bottom
|
|
||||||
.map(|v| v.to_pixels(base_size, rem_size))
|
|
||||||
.unwrap_or(px(0.)),
|
|
||||||
};
|
|
||||||
|
|
||||||
state.editor_scrollbar_paddings.set(paddings);
|
|
||||||
state.editor_scrollbar_snapshot.set(None);
|
|
||||||
|
|
||||||
v_flex().size_full().child(
|
|
||||||
div()
|
|
||||||
.relative()
|
|
||||||
.flex_1()
|
|
||||||
.child(input_state.clone())
|
|
||||||
.child(EditorScrollbar::new(input_state.clone())),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Styled for Input {
|
impl<M: InputModeKind> Styled for Input<M> {
|
||||||
fn style(&mut self) -> &mut StyleRefinement {
|
fn style(&mut self) -> &mut StyleRefinement {
|
||||||
&mut self.style
|
&mut self.style
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderOnce for Input {
|
impl<M: InputModeKind> RenderOnce for Input<M> {
|
||||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
const LINE_HEIGHT: Rems = Rems(1.25);
|
const LINE_HEIGHT: Rems = Rems(1.25);
|
||||||
let text_align = self.style.text.text_align.unwrap_or(TextAlign::Left);
|
let text_align = self.style.text.text_align.unwrap_or(TextAlign::Left);
|
||||||
|
|
||||||
self.state.update(cx, |state, _| {
|
let multi_line = self.state.read(cx).is_multi_line();
|
||||||
state.disabled = self.disabled;
|
let editor_paddings = if multi_line {
|
||||||
state.size = self.size;
|
input_paddings(self.size, &self.style, window)
|
||||||
// Only for single line mode
|
} else {
|
||||||
if state.mode.is_single_line() {
|
Edges::default()
|
||||||
state.text_align = text_align;
|
};
|
||||||
|
self.state.update(cx, |state, cx| {
|
||||||
|
state.set_editor_style(input_editor_style(cx));
|
||||||
|
state.set_editor_paddings(editor_paddings);
|
||||||
|
state.set_disabled(self.disabled, cx);
|
||||||
|
if state.is_single_line() {
|
||||||
|
state.set_text_align(text_align, cx);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let state = self.state.read(cx);
|
let state = self.state.read(cx);
|
||||||
let _focused = state.focus_handle.is_focused(window) && !state.disabled;
|
let presentation = state.presentation();
|
||||||
|
let disabled = presentation.is_disabled();
|
||||||
|
let loading = presentation.is_loading();
|
||||||
|
let text_is_empty = state.text().len() == 0;
|
||||||
|
|
||||||
let gap_x = match self.size {
|
let gap_x = match self.size {
|
||||||
Size::Small => px(4.),
|
Size::Small => px(4.),
|
||||||
@@ -216,117 +227,49 @@ impl RenderOnce for Input {
|
|||||||
_ => px(6.),
|
_ => px(6.),
|
||||||
};
|
};
|
||||||
|
|
||||||
let (bg, _) = input_style(state.disabled, cx);
|
let background = input_background(disabled, cx);
|
||||||
|
let show_clear_button =
|
||||||
|
self.cleanable && state.is_editable() && !loading && !text_is_empty && !multi_line;
|
||||||
|
let has_suffix = self.suffix.is_some() || loading || self.mask_toggle || show_clear_button;
|
||||||
|
|
||||||
let prefix = self.prefix;
|
let prefix = self.prefix;
|
||||||
let suffix = self.suffix;
|
let suffix = self.suffix;
|
||||||
let show_clear_button = self.cleanable
|
let state_entity = self.state.clone();
|
||||||
&& !state.disabled
|
|
||||||
&& !state.loading
|
|
||||||
&& state.text.len() > 0
|
|
||||||
&& state.mode.is_single_line();
|
|
||||||
let has_suffix = suffix.is_some() || state.loading || self.mask_toggle || show_clear_button;
|
|
||||||
|
|
||||||
div()
|
InputBase::new(("input", self.state.entity_id()))
|
||||||
.id(("input", self.state.entity_id()))
|
|
||||||
.flex()
|
.flex()
|
||||||
.key_context(crate::input::CONTEXT)
|
|
||||||
.track_focus(&state.focus_handle.clone())
|
|
||||||
.tab_index(self.tab_index)
|
|
||||||
.when(!state.disabled, |this| {
|
|
||||||
this.on_action(window.listener_for(&self.state, InputState::backspace))
|
|
||||||
.on_action(window.listener_for(&self.state, InputState::delete))
|
|
||||||
.on_action(
|
|
||||||
window.listener_for(&self.state, InputState::delete_to_beginning_of_line),
|
|
||||||
)
|
|
||||||
.on_action(window.listener_for(&self.state, InputState::delete_to_end_of_line))
|
|
||||||
.on_action(window.listener_for(&self.state, InputState::delete_previous_word))
|
|
||||||
.on_action(window.listener_for(&self.state, InputState::delete_next_word))
|
|
||||||
.on_action(window.listener_for(&self.state, InputState::enter))
|
|
||||||
.on_action(window.listener_for(&self.state, InputState::escape))
|
|
||||||
.on_action(window.listener_for(&self.state, InputState::paste))
|
|
||||||
.on_action(window.listener_for(&self.state, InputState::cut))
|
|
||||||
.on_action(window.listener_for(&self.state, InputState::undo))
|
|
||||||
.on_action(window.listener_for(&self.state, InputState::redo))
|
|
||||||
.when(state.mode.is_multi_line(), |this| {
|
|
||||||
this.on_action(window.listener_for(&self.state, InputState::indent_inline))
|
|
||||||
.on_action(window.listener_for(&self.state, InputState::outdent_inline))
|
|
||||||
.on_action(window.listener_for(&self.state, InputState::indent_block))
|
|
||||||
.on_action(window.listener_for(&self.state, InputState::outdent_block))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.on_action(window.listener_for(&self.state, InputState::left))
|
|
||||||
.on_action(window.listener_for(&self.state, InputState::right))
|
|
||||||
.on_action(window.listener_for(&self.state, InputState::select_left))
|
|
||||||
.on_action(window.listener_for(&self.state, InputState::select_right))
|
|
||||||
.when(state.mode.is_multi_line(), |this| {
|
|
||||||
this.on_action(window.listener_for(&self.state, InputState::up))
|
|
||||||
.on_action(window.listener_for(&self.state, InputState::down))
|
|
||||||
.on_action(window.listener_for(&self.state, InputState::select_up))
|
|
||||||
.on_action(window.listener_for(&self.state, InputState::select_down))
|
|
||||||
.on_action(window.listener_for(&self.state, InputState::page_up))
|
|
||||||
.on_action(window.listener_for(&self.state, InputState::page_down))
|
|
||||||
})
|
|
||||||
.on_action(window.listener_for(&self.state, InputState::select_all))
|
|
||||||
.on_action(window.listener_for(&self.state, InputState::select_to_start_of_line))
|
|
||||||
.on_action(window.listener_for(&self.state, InputState::select_to_end_of_line))
|
|
||||||
.on_action(window.listener_for(&self.state, InputState::select_to_previous_word))
|
|
||||||
.on_action(window.listener_for(&self.state, InputState::select_to_next_word))
|
|
||||||
.on_action(window.listener_for(&self.state, InputState::home))
|
|
||||||
.on_action(window.listener_for(&self.state, InputState::end))
|
|
||||||
.on_action(window.listener_for(&self.state, InputState::move_to_start))
|
|
||||||
.on_action(window.listener_for(&self.state, InputState::move_to_end))
|
|
||||||
.on_action(window.listener_for(&self.state, InputState::move_to_previous_word))
|
|
||||||
.on_action(window.listener_for(&self.state, InputState::move_to_next_word))
|
|
||||||
.on_action(window.listener_for(&self.state, InputState::select_to_start))
|
|
||||||
.on_action(window.listener_for(&self.state, InputState::select_to_end))
|
|
||||||
.on_action(window.listener_for(&self.state, InputState::show_character_palette))
|
|
||||||
.on_action(window.listener_for(&self.state, InputState::copy))
|
|
||||||
.on_key_down(window.listener_for(&self.state, InputState::on_key_down))
|
|
||||||
.on_mouse_down(
|
|
||||||
MouseButton::Left,
|
|
||||||
window.listener_for(&self.state, InputState::on_mouse_down),
|
|
||||||
)
|
|
||||||
.on_mouse_down(
|
|
||||||
MouseButton::Right,
|
|
||||||
window.listener_for(&self.state, InputState::on_mouse_down),
|
|
||||||
)
|
|
||||||
.on_mouse_up(
|
|
||||||
MouseButton::Left,
|
|
||||||
window.listener_for(&self.state, InputState::on_mouse_up),
|
|
||||||
)
|
|
||||||
.on_mouse_up(
|
|
||||||
MouseButton::Right,
|
|
||||||
window.listener_for(&self.state, InputState::on_mouse_up),
|
|
||||||
)
|
|
||||||
.on_scroll_wheel(window.listener_for(&self.state, InputState::on_scroll_wheel))
|
|
||||||
.size_full()
|
.size_full()
|
||||||
.line_height(LINE_HEIGHT)
|
.line_height(LINE_HEIGHT)
|
||||||
.input_px(self.size)
|
.when(!multi_line, |this| {
|
||||||
.input_py(self.size)
|
this.input_px(self.size).input_py(self.size)
|
||||||
|
})
|
||||||
.input_h(self.size)
|
.input_h(self.size)
|
||||||
.input_font_size(self.size)
|
.when(!disabled, |this| this.cursor_text())
|
||||||
.when(!self.disabled, |this| this.cursor_text())
|
.on_mouse_down(MouseButton::Left, {
|
||||||
|
let state_entity = state_entity.clone();
|
||||||
|
move |_, window, cx| state_entity.update(cx, |state, cx| state.focus(window, cx))
|
||||||
|
})
|
||||||
.items_center()
|
.items_center()
|
||||||
.when(state.mode.is_multi_line(), |this| {
|
.when(multi_line, |this| {
|
||||||
this.h_auto()
|
this.h_auto()
|
||||||
.when_some(self.height, |this, height| this.h(height))
|
.when_some(self.height, |this, height| this.h(height))
|
||||||
})
|
})
|
||||||
.when(self.appearance, |this| {
|
.when(self.appearance, |this| {
|
||||||
this.bg(bg)
|
this.bg(background)
|
||||||
.when(self.disabled, |this| this.opacity(0.5))
|
.when(self.disabled, |this| this.opacity(0.5))
|
||||||
.rounded(cx.theme().radius)
|
.rounded(cx.theme().radius)
|
||||||
})
|
})
|
||||||
.items_center()
|
.tab_index(self.tab_index)
|
||||||
.gap(gap_x)
|
.gap(gap_x)
|
||||||
.refine_style(&self.style)
|
.refine_style(&self.style)
|
||||||
.children(prefix)
|
.children(prefix)
|
||||||
.when(state.mode.is_multi_line(), |mut this| {
|
.when(!multi_line, |this| this.child(state_entity.clone()))
|
||||||
let paddings = this.style().padding.clone();
|
.when(multi_line, |this| {
|
||||||
this.child(Self::render_editor(paddings, &self.state, state, window))
|
this.child(
|
||||||
})
|
v_flex()
|
||||||
.when(!state.mode.is_multi_line(), |this| {
|
.size_full()
|
||||||
this.child(self.state.clone())
|
.child(div().relative().flex_1().child(state_entity.clone())),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.when(has_suffix, |this| {
|
.when(has_suffix, |this| {
|
||||||
this.pr_2().child(
|
this.pr_2().child(
|
||||||
@@ -334,13 +277,13 @@ impl RenderOnce for Input {
|
|||||||
.id("suffix")
|
.id("suffix")
|
||||||
.gap(gap_x)
|
.gap(gap_x)
|
||||||
.items_center()
|
.items_center()
|
||||||
.when(state.loading, |this| this.child(Indicator::new()))
|
.when(loading, |this| this.child(Indicator::new()))
|
||||||
.when(self.mask_toggle, |this| {
|
.when(self.mask_toggle, |this| {
|
||||||
this.child(Self::render_toggle_mask_button(&self.state, cx))
|
this.child(Self::render_toggle_mask_button(&state_entity))
|
||||||
})
|
})
|
||||||
.when(show_clear_button, |this| {
|
.when(show_clear_button, |this| {
|
||||||
this.child(clear_button(cx).on_click({
|
this.child(clear_button(cx).on_click({
|
||||||
let state = self.state.clone();
|
let state = state_entity.clone();
|
||||||
move |_, window, cx| {
|
move |_, window, cx| {
|
||||||
state.update(cx, |state, cx| {
|
state.update(cx, |state, cx| {
|
||||||
state.clean(window, cx);
|
state.clean(window, cx);
|
||||||
|
|||||||
@@ -1,409 +0,0 @@
|
|||||||
use gpui::SharedString;
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Debug)]
|
|
||||||
pub enum MaskToken {
|
|
||||||
/// 0 Digit, equivalent to `[0]`
|
|
||||||
// Digit0,
|
|
||||||
/// Digit, equivalent to `[0-9]`
|
|
||||||
Digit,
|
|
||||||
/// Letter, equivalent to `[a-zA-Z]`
|
|
||||||
Letter,
|
|
||||||
/// Letter or digit, equivalent to `[a-zA-Z0-9]`
|
|
||||||
LetterOrDigit,
|
|
||||||
/// Separator
|
|
||||||
Sep(char),
|
|
||||||
/// Any character
|
|
||||||
Any,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(unused)]
|
|
||||||
impl MaskToken {
|
|
||||||
/// Check if the token is any character.
|
|
||||||
pub fn is_any(&self) -> bool {
|
|
||||||
matches!(self, MaskToken::Any)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if the token is a match for the given character.
|
|
||||||
///
|
|
||||||
/// The separator is always a match any input character.
|
|
||||||
fn is_match(&self, ch: char) -> bool {
|
|
||||||
match self {
|
|
||||||
MaskToken::Digit => ch.is_ascii_digit(),
|
|
||||||
MaskToken::Letter => ch.is_ascii_alphabetic(),
|
|
||||||
MaskToken::LetterOrDigit => ch.is_ascii_alphanumeric(),
|
|
||||||
MaskToken::Any => true,
|
|
||||||
MaskToken::Sep(c) => *c == ch,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Is the token a separator (Can be ignored)
|
|
||||||
fn is_sep(&self) -> bool {
|
|
||||||
matches!(self, MaskToken::Sep(_))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if the token is a number.
|
|
||||||
pub fn is_number(&self) -> bool {
|
|
||||||
matches!(self, MaskToken::Digit)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn placeholder(&self) -> char {
|
|
||||||
match self {
|
|
||||||
MaskToken::Sep(c) => *c,
|
|
||||||
_ => '_',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn mask_char(&self, ch: char) -> char {
|
|
||||||
match self {
|
|
||||||
MaskToken::Digit | MaskToken::LetterOrDigit | MaskToken::Letter => ch,
|
|
||||||
MaskToken::Sep(c) => *c,
|
|
||||||
MaskToken::Any => ch,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn unmask_char(&self, ch: char) -> Option<char> {
|
|
||||||
match self {
|
|
||||||
MaskToken::Digit => Some(ch),
|
|
||||||
MaskToken::Letter => Some(ch),
|
|
||||||
MaskToken::LetterOrDigit => Some(ch),
|
|
||||||
MaskToken::Any => Some(ch),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
|
||||||
pub enum MaskPattern {
|
|
||||||
#[default]
|
|
||||||
None,
|
|
||||||
Pattern {
|
|
||||||
pattern: SharedString,
|
|
||||||
tokens: Vec<MaskToken>,
|
|
||||||
},
|
|
||||||
Number {
|
|
||||||
/// Group separator, e.g. "," or " "
|
|
||||||
separator: Option<char>,
|
|
||||||
/// Number of fraction digits, e.g. 2 for 123.45
|
|
||||||
fraction: Option<usize>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&str> for MaskPattern {
|
|
||||||
fn from(pattern: &str) -> Self {
|
|
||||||
Self::new(pattern)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MaskPattern {
|
|
||||||
/// Create a new mask pattern
|
|
||||||
///
|
|
||||||
/// - `9` - Digit
|
|
||||||
/// - `A` - Letter
|
|
||||||
/// - `#` - Letter or Digit
|
|
||||||
/// - `*` - Any character
|
|
||||||
/// - other characters - Separator
|
|
||||||
///
|
|
||||||
/// For example:
|
|
||||||
///
|
|
||||||
/// - `(999)999-9999` - US phone number: (123)456-7890
|
|
||||||
/// - `99999-9999` - ZIP code: 12345-6789
|
|
||||||
/// - `AAAA-99-####` - Custom pattern: ABCD-12-3AB4
|
|
||||||
/// - `*999*` - Custom pattern: (123) or [123]
|
|
||||||
pub fn new(pattern: &str) -> Self {
|
|
||||||
let tokens = pattern
|
|
||||||
.chars()
|
|
||||||
.map(|ch| match ch {
|
|
||||||
// '0' => MaskToken::Digit0,
|
|
||||||
'9' => MaskToken::Digit,
|
|
||||||
'A' => MaskToken::Letter,
|
|
||||||
'#' => MaskToken::LetterOrDigit,
|
|
||||||
'*' => MaskToken::Any,
|
|
||||||
_ => MaskToken::Sep(ch),
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Self::Pattern {
|
|
||||||
pattern: pattern.to_owned().into(),
|
|
||||||
tokens,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(unused)]
|
|
||||||
fn tokens(&self) -> Option<&Vec<MaskToken>> {
|
|
||||||
match self {
|
|
||||||
Self::Pattern { tokens, .. } => Some(tokens),
|
|
||||||
Self::Number { .. } => None,
|
|
||||||
Self::None => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new mask pattern with group separator, e.g. "," or " "
|
|
||||||
pub fn number(sep: Option<char>) -> Self {
|
|
||||||
Self::Number {
|
|
||||||
separator: sep,
|
|
||||||
fraction: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn placeholder(&self) -> Option<String> {
|
|
||||||
match self {
|
|
||||||
Self::Pattern { tokens, .. } => {
|
|
||||||
Some(tokens.iter().map(|token| token.placeholder()).collect())
|
|
||||||
}
|
|
||||||
Self::Number { .. } => None,
|
|
||||||
Self::None => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return true if the mask pattern is None or no any pattern.
|
|
||||||
pub fn is_none(&self) -> bool {
|
|
||||||
match self {
|
|
||||||
Self::Pattern { tokens, .. } => tokens.is_empty(),
|
|
||||||
Self::Number { .. } => false,
|
|
||||||
Self::None => true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check is the mask text is valid.
|
|
||||||
///
|
|
||||||
/// If the mask pattern is None, always return true.
|
|
||||||
pub fn is_valid(&self, mask_text: &str) -> bool {
|
|
||||||
if self.is_none() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut text_index = 0;
|
|
||||||
let mask_text_chars: Vec<char> = mask_text.chars().collect();
|
|
||||||
match self {
|
|
||||||
Self::Pattern { tokens, .. } => {
|
|
||||||
for token in tokens {
|
|
||||||
if text_index >= mask_text_chars.len() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
let ch = mask_text_chars[text_index];
|
|
||||||
if token.is_match(ch) {
|
|
||||||
text_index += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
text_index == mask_text.len()
|
|
||||||
}
|
|
||||||
Self::Number { separator, .. } => {
|
|
||||||
if mask_text.is_empty() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if the text is valid number
|
|
||||||
let mut parts = mask_text.split('.');
|
|
||||||
let int_part = parts.next().unwrap_or("");
|
|
||||||
let frac_part = parts.next();
|
|
||||||
|
|
||||||
if int_part.is_empty() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let sign_positions: Vec<usize> = int_part
|
|
||||||
.chars()
|
|
||||||
.enumerate()
|
|
||||||
.filter_map(|(i, ch)| match is_sign(&ch) {
|
|
||||||
true => Some(i),
|
|
||||||
false => None,
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// only one sign is valid
|
|
||||||
// sign is only valid at the beginning of the string
|
|
||||||
if sign_positions.len() > 1 || sign_positions.first() > Some(&0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if the integer part is valid
|
|
||||||
if !int_part.chars().enumerate().all(|(i, ch)| {
|
|
||||||
ch.is_ascii_digit() || is_sign(&ch) && i == 0 || Some(ch) == *separator
|
|
||||||
}) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if the fraction part is valid
|
|
||||||
if let Some(frac) = frac_part
|
|
||||||
&& !frac
|
|
||||||
.chars()
|
|
||||||
.all(|ch| ch.is_ascii_digit() || Some(ch) == *separator)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
Self::None => true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if valid input char at the given position.
|
|
||||||
pub fn is_valid_at(&self, ch: char, pos: usize) -> bool {
|
|
||||||
if self.is_none() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
match self {
|
|
||||||
Self::Pattern { tokens, .. } => {
|
|
||||||
if let Some(token) = tokens.get(pos) {
|
|
||||||
if token.is_match(ch) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if token.is_sep() {
|
|
||||||
// If next token is match, it's valid
|
|
||||||
if let Some(next_token) = tokens.get(pos + 1)
|
|
||||||
&& next_token.is_match(ch)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
false
|
|
||||||
}
|
|
||||||
Self::Number { .. } => true,
|
|
||||||
Self::None => true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Format the text according to the mask pattern
|
|
||||||
///
|
|
||||||
/// For example:
|
|
||||||
///
|
|
||||||
/// - pattern: (999)999-999
|
|
||||||
/// - text: 123456789
|
|
||||||
/// - mask_text: (123)456-789
|
|
||||||
pub fn mask(&self, text: &str) -> SharedString {
|
|
||||||
if self.is_none() {
|
|
||||||
return text.to_owned().into();
|
|
||||||
}
|
|
||||||
|
|
||||||
match self {
|
|
||||||
Self::Number {
|
|
||||||
separator,
|
|
||||||
fraction,
|
|
||||||
} => {
|
|
||||||
if let Some(sep) = *separator {
|
|
||||||
// Remove the existing group separator
|
|
||||||
let text = text.replace(sep, "");
|
|
||||||
|
|
||||||
let mut parts = text.split('.');
|
|
||||||
let int_part = parts.next().unwrap_or("");
|
|
||||||
|
|
||||||
// Limit the fraction part to the given range, if not enough, pad with 0
|
|
||||||
let frac_part = parts.next().map(|part| {
|
|
||||||
part.chars()
|
|
||||||
.take(fraction.unwrap_or(usize::MAX))
|
|
||||||
.collect::<String>()
|
|
||||||
});
|
|
||||||
|
|
||||||
// Reverse the integer part for easier grouping
|
|
||||||
let mut chars: Vec<char> = int_part.chars().rev().collect();
|
|
||||||
|
|
||||||
// Removing the sign from formatting to avoid cases such as: -,123
|
|
||||||
let maybe_signed = chars.iter().position(is_sign).map(|pos| chars.remove(pos));
|
|
||||||
|
|
||||||
let mut result = String::new();
|
|
||||||
for (i, ch) in chars.iter().enumerate() {
|
|
||||||
if i > 0 && i % 3 == 0 {
|
|
||||||
result.push(sep);
|
|
||||||
}
|
|
||||||
result.push(*ch);
|
|
||||||
}
|
|
||||||
let int_with_sep: String = result.chars().rev().collect();
|
|
||||||
|
|
||||||
let final_str = if let Some(frac) = frac_part {
|
|
||||||
if fraction == &Some(0) {
|
|
||||||
int_with_sep
|
|
||||||
} else {
|
|
||||||
format!("{}.{}", int_with_sep, frac)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
int_with_sep
|
|
||||||
};
|
|
||||||
|
|
||||||
let final_str = if let Some(sign) = maybe_signed {
|
|
||||||
format!("{}{}", sign, final_str)
|
|
||||||
} else {
|
|
||||||
final_str
|
|
||||||
};
|
|
||||||
|
|
||||||
return final_str.into();
|
|
||||||
}
|
|
||||||
|
|
||||||
text.to_owned().into()
|
|
||||||
}
|
|
||||||
Self::Pattern { tokens, .. } => {
|
|
||||||
let mut result = String::new();
|
|
||||||
let mut text_index = 0;
|
|
||||||
let text_chars: Vec<char> = text.chars().collect();
|
|
||||||
for (pos, token) in tokens.iter().enumerate() {
|
|
||||||
if text_index >= text_chars.len() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
let ch = text_chars[text_index];
|
|
||||||
// Break if expected char is not match
|
|
||||||
if !token.is_sep() && !self.is_valid_at(ch, pos) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
let mask_ch = token.mask_char(ch);
|
|
||||||
result.push(mask_ch);
|
|
||||||
if ch == mask_ch {
|
|
||||||
text_index += 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result.into()
|
|
||||||
}
|
|
||||||
Self::None => text.to_owned().into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extract original text from masked text
|
|
||||||
pub fn unmask(&self, mask_text: &str) -> String {
|
|
||||||
match self {
|
|
||||||
Self::Number { separator, .. } => {
|
|
||||||
if let Some(sep) = *separator {
|
|
||||||
let mut result = String::new();
|
|
||||||
for ch in mask_text.chars() {
|
|
||||||
if ch == sep {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
result.push(ch);
|
|
||||||
}
|
|
||||||
|
|
||||||
if result.contains('.') {
|
|
||||||
result = result.trim_end_matches('0').to_string();
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
mask_text.to_owned()
|
|
||||||
}
|
|
||||||
Self::Pattern { tokens, .. } => {
|
|
||||||
let mut result = String::new();
|
|
||||||
let mask_text_chars: Vec<char> = mask_text.chars().collect();
|
|
||||||
for (text_index, token) in tokens.iter().enumerate() {
|
|
||||||
if text_index >= mask_text_chars.len() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
let ch = mask_text_chars[text_index];
|
|
||||||
let unmask_ch = token.unmask_char(ch);
|
|
||||||
if let Some(ch) = unmask_ch {
|
|
||||||
result.push(ch);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result
|
|
||||||
}
|
|
||||||
Self::None => mask_text.to_owned(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn is_sign(ch: &char) -> bool {
|
|
||||||
matches!(ch, '+' | '-')
|
|
||||||
}
|
|
||||||
@@ -1,27 +1,7 @@
|
|||||||
pub(super) const MASK_CHAR: char = '*';
|
|
||||||
|
|
||||||
mod blink_cursor;
|
|
||||||
mod change;
|
|
||||||
mod clear_button;
|
mod clear_button;
|
||||||
mod cursor;
|
|
||||||
mod display_map;
|
|
||||||
mod element;
|
|
||||||
mod indent;
|
|
||||||
#[allow(clippy::module_inception)]
|
#[allow(clippy::module_inception)]
|
||||||
mod input;
|
mod input;
|
||||||
mod mask_pattern;
|
|
||||||
mod mode;
|
|
||||||
mod movement;
|
|
||||||
mod rope_ext;
|
|
||||||
mod selection;
|
|
||||||
mod state;
|
|
||||||
|
|
||||||
pub(crate) use clear_button::*;
|
pub(crate) use clear_button::*;
|
||||||
pub use cursor::*;
|
pub use gpui_base::input::{InputEvent, InputState, TextareaState};
|
||||||
pub use display_map::DisplayMap;
|
|
||||||
pub use indent::TabSize;
|
|
||||||
pub use input::*;
|
pub use input::*;
|
||||||
pub use mask_pattern::MaskPattern;
|
|
||||||
pub use rope_ext::{InputEdit, Point, RopeExt, RopeLines};
|
|
||||||
pub use ropey::Rope;
|
|
||||||
pub use state::*;
|
|
||||||
|
|||||||
@@ -1,145 +0,0 @@
|
|||||||
use super::display_map::DisplayMap;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub(crate) enum InputMode {
|
|
||||||
/// A plain text input mode.
|
|
||||||
PlainText {
|
|
||||||
multi_line: bool,
|
|
||||||
tab: crate::input::indent::TabSize,
|
|
||||||
rows: usize,
|
|
||||||
},
|
|
||||||
/// An auto grow input mode.
|
|
||||||
AutoGrow {
|
|
||||||
rows: usize,
|
|
||||||
min_rows: usize,
|
|
||||||
max_rows: usize,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for InputMode {
|
|
||||||
fn default() -> Self {
|
|
||||||
InputMode::plain_text()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(unused)]
|
|
||||||
impl InputMode {
|
|
||||||
/// Create a plain input mode with default settings.
|
|
||||||
pub(super) fn plain_text() -> Self {
|
|
||||||
InputMode::PlainText {
|
|
||||||
multi_line: false,
|
|
||||||
tab: crate::input::indent::TabSize::default(),
|
|
||||||
rows: 1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create an auto grow input mode with given min and max rows.
|
|
||||||
pub(super) fn auto_grow(min_rows: usize, max_rows: usize) -> Self {
|
|
||||||
InputMode::AutoGrow {
|
|
||||||
rows: min_rows,
|
|
||||||
min_rows,
|
|
||||||
max_rows,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn multi_line(mut self, multi_line: bool) -> Self {
|
|
||||||
match &mut self {
|
|
||||||
InputMode::PlainText { multi_line: ml, .. } => *ml = multi_line,
|
|
||||||
InputMode::AutoGrow { .. } => {}
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub(super) fn is_single_line(&self) -> bool {
|
|
||||||
!self.is_multi_line()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub(super) fn is_auto_grow(&self) -> bool {
|
|
||||||
matches!(self, InputMode::AutoGrow { .. })
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub(super) fn is_multi_line(&self) -> bool {
|
|
||||||
match self {
|
|
||||||
InputMode::PlainText { multi_line, .. } => *multi_line,
|
|
||||||
InputMode::AutoGrow { max_rows, .. } => *max_rows > 1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn set_rows(&mut self, new_rows: usize) {
|
|
||||||
match self {
|
|
||||||
InputMode::PlainText { rows, .. } => {
|
|
||||||
*rows = new_rows;
|
|
||||||
}
|
|
||||||
InputMode::AutoGrow {
|
|
||||||
rows,
|
|
||||||
min_rows,
|
|
||||||
max_rows,
|
|
||||||
} => {
|
|
||||||
*rows = new_rows.clamp(*min_rows, *max_rows);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn update_auto_grow(&mut self, display_map: &DisplayMap) {
|
|
||||||
if self.is_single_line() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let wrapped_lines = display_map.wrap_row_count();
|
|
||||||
self.set_rows(wrapped_lines);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// At least 1 row be return.
|
|
||||||
pub(super) fn rows(&self) -> usize {
|
|
||||||
if !self.is_multi_line() {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
match self {
|
|
||||||
InputMode::PlainText { rows, .. } => *rows,
|
|
||||||
InputMode::AutoGrow { rows, .. } => *rows,
|
|
||||||
}
|
|
||||||
.max(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// At least 1 row be return.
|
|
||||||
#[allow(unused)]
|
|
||||||
pub(super) fn min_rows(&self) -> usize {
|
|
||||||
match self {
|
|
||||||
InputMode::AutoGrow { min_rows, .. } => *min_rows,
|
|
||||||
_ => 1,
|
|
||||||
}
|
|
||||||
.max(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(unused)]
|
|
||||||
pub(super) fn max_rows(&self) -> usize {
|
|
||||||
if !self.is_multi_line() {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
match self {
|
|
||||||
InputMode::AutoGrow { max_rows, .. } => *max_rows,
|
|
||||||
_ => usize::MAX,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub(super) fn is_indentable(&self) -> bool {
|
|
||||||
match self {
|
|
||||||
InputMode::PlainText { multi_line, .. } => *multi_line,
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub(super) fn tab_size(&self) -> crate::input::indent::TabSize {
|
|
||||||
match self {
|
|
||||||
InputMode::PlainText { tab, .. } => *tab,
|
|
||||||
_ => crate::input::indent::TabSize::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,264 +0,0 @@
|
|||||||
use gpui::{Context, Point, Window};
|
|
||||||
|
|
||||||
use crate::input::{
|
|
||||||
InputState, MoveDown, MoveEnd, MoveHome, MoveLeft, MovePageDown, MovePageUp, MoveRight,
|
|
||||||
MoveToEnd, MoveToNextWord, MoveToPreviousWord, MoveToStart, MoveUp, RopeExt as _,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub(crate) enum MoveDirection {
|
|
||||||
Up,
|
|
||||||
Down,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl InputState {
|
|
||||||
/// Called after moving the cursor. Updates preferred_column if we know where the cursor now is.
|
|
||||||
pub(super) fn update_preferred_column(&mut self) {
|
|
||||||
let Some(last_layout) = &self.last_layout else {
|
|
||||||
self.preferred_column = None;
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let point = self.text.offset_to_point(self.cursor());
|
|
||||||
let Some(line) = last_layout.line(point.row) else {
|
|
||||||
self.preferred_column = None;
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let Some(pos) = line.position_for_index(point.column, last_layout, false) else {
|
|
||||||
self.preferred_column = None;
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
self.preferred_column = Some((pos.x, point.column));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Move the cursor to the given offset.
|
|
||||||
///
|
|
||||||
/// The offset is the UTF-8 offset.
|
|
||||||
///
|
|
||||||
/// Ensure the offset use self.next_boundary or self.previous_boundary to get the correct offset.
|
|
||||||
pub(crate) fn move_to(
|
|
||||||
&mut self,
|
|
||||||
offset: usize,
|
|
||||||
direction: Option<MoveDirection>,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
let offset = offset.clamp(0, self.text.len());
|
|
||||||
self.cursor_line_end_affinity = false;
|
|
||||||
self.selected_range = (offset..offset).into();
|
|
||||||
self.scroll_to(offset, direction, cx);
|
|
||||||
self.pause_blink_cursor(cx);
|
|
||||||
self.update_preferred_column();
|
|
||||||
cx.notify()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Move the cursor vertically by one line (up or down) while preserving the column if possible.
|
|
||||||
///
|
|
||||||
/// move_lines: Number of lines to move vertically (positive for down, negative for up).
|
|
||||||
pub(super) fn move_vertical(
|
|
||||||
&mut self,
|
|
||||||
move_lines: isize,
|
|
||||||
_: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
if self.mode.is_single_line() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let Some(last_layout) = &self.last_layout else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let offset = self.cursor();
|
|
||||||
let was_preferred_column = self.preferred_column;
|
|
||||||
|
|
||||||
let mut display_point = self.display_map.offset_to_wrap_display_point(offset);
|
|
||||||
|
|
||||||
// Convert wrap row → display row (skips folded rows), move, then convert back
|
|
||||||
let current_display_row = self
|
|
||||||
.display_map
|
|
||||||
.wrap_row_to_display_row(display_point.row)
|
|
||||||
.unwrap_or_else(|| {
|
|
||||||
self.display_map
|
|
||||||
.nearest_visible_display_row(display_point.row)
|
|
||||||
});
|
|
||||||
let max_display_row = self.display_map.display_row_count().saturating_sub(1);
|
|
||||||
let target_display_row = current_display_row
|
|
||||||
.saturating_add_signed(move_lines)
|
|
||||||
.min(max_display_row);
|
|
||||||
let target_wrap_row = self
|
|
||||||
.display_map
|
|
||||||
.display_row_to_wrap_row(target_display_row)
|
|
||||||
.unwrap_or(display_point.row);
|
|
||||||
|
|
||||||
display_point.row = target_wrap_row;
|
|
||||||
display_point.column = 0;
|
|
||||||
let mut new_offset = self.display_map.wrap_display_point_to_offset(display_point);
|
|
||||||
|
|
||||||
if let Some((preferred_x, column)) = was_preferred_column {
|
|
||||||
// Get display point again to update local_row.
|
|
||||||
let mut next_display_point = self.display_map.offset_to_wrap_display_point(new_offset);
|
|
||||||
next_display_point.column = 0;
|
|
||||||
let next_point = self
|
|
||||||
.display_map
|
|
||||||
.wrap_display_point_to_point(next_display_point);
|
|
||||||
let line_start_offset = self.text.line_start_offset(next_point.row);
|
|
||||||
|
|
||||||
// If in visible range, prefer to use position to get column.
|
|
||||||
if let Some(line) = last_layout.line(next_point.row) {
|
|
||||||
if let Some(x) = line.closest_index_for_position(
|
|
||||||
Point {
|
|
||||||
x: preferred_x,
|
|
||||||
y: next_display_point.local_row * last_layout.line_height,
|
|
||||||
},
|
|
||||||
last_layout,
|
|
||||||
) {
|
|
||||||
new_offset = line_start_offset + x;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Not in visible range, use column directly.
|
|
||||||
let max_line_len = self.text.slice_line(next_point.row).len();
|
|
||||||
new_offset = line_start_offset + column.min(max_line_len);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.pause_blink_cursor(cx);
|
|
||||||
let direction = if move_lines < 0 {
|
|
||||||
MoveDirection::Up
|
|
||||||
} else {
|
|
||||||
MoveDirection::Down
|
|
||||||
};
|
|
||||||
self.move_to(new_offset, Some(direction), cx);
|
|
||||||
// Set back the preferred_column
|
|
||||||
self.preferred_column = was_preferred_column;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn left(&mut self, _: &MoveLeft, _: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
self.pause_blink_cursor(cx);
|
|
||||||
if self.selected_range.is_empty() {
|
|
||||||
self.move_to(self.previous_boundary(self.cursor()), None, cx);
|
|
||||||
} else {
|
|
||||||
self.move_to(self.selected_range.start, None, cx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn right(&mut self, _: &MoveRight, _: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
self.pause_blink_cursor(cx);
|
|
||||||
if self.selected_range.is_empty() {
|
|
||||||
self.move_to(self.next_boundary(self.selected_range.end), None, cx);
|
|
||||||
} else {
|
|
||||||
self.move_to(self.selected_range.end, None, cx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn up(&mut self, _action: &MoveUp, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
if self.mode.is_single_line() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.selected_range.is_empty() {
|
|
||||||
self.move_to(
|
|
||||||
self.previous_boundary(self.selected_range.start.saturating_sub(1)),
|
|
||||||
Some(MoveDirection::Up),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
self.pause_blink_cursor(cx);
|
|
||||||
self.move_vertical(-1, window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn down(&mut self, _action: &MoveDown, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
if self.mode.is_single_line() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.selected_range.is_empty() {
|
|
||||||
self.move_to(
|
|
||||||
self.next_boundary(self.selected_range.end.saturating_sub(1)),
|
|
||||||
Some(MoveDirection::Down),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.pause_blink_cursor(cx);
|
|
||||||
self.move_vertical(1, window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn page_up(&mut self, _: &MovePageUp, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
if self.mode.is_single_line() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let Some(last_layout) = &self.last_layout else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let display_lines = (self.input_bounds.size.height / last_layout.line_height) as isize;
|
|
||||||
self.move_vertical(-display_lines, window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn page_down(
|
|
||||||
&mut self,
|
|
||||||
_: &MovePageDown,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
if self.mode.is_single_line() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let Some(last_layout) = &self.last_layout else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let display_lines = (self.input_bounds.size.height / last_layout.line_height) as isize;
|
|
||||||
self.move_vertical(display_lines, window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn home(&mut self, _: &MoveHome, _: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
self.pause_blink_cursor(cx);
|
|
||||||
let offset = self.start_of_line();
|
|
||||||
self.move_to(offset, Some(MoveDirection::Up), cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn end(&mut self, _: &MoveEnd, _: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
self.pause_blink_cursor(cx);
|
|
||||||
let offset = self.end_of_line();
|
|
||||||
self.move_to(offset, Some(MoveDirection::Down), cx);
|
|
||||||
self.cursor_line_end_affinity = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn move_to_start(
|
|
||||||
&mut self,
|
|
||||||
_: &MoveToStart,
|
|
||||||
_: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
self.move_to(0, None, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn move_to_end(&mut self, _: &MoveToEnd, _: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
self.move_to(self.text.len(), None, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn move_to_previous_word(
|
|
||||||
&mut self,
|
|
||||||
_: &MoveToPreviousWord,
|
|
||||||
_: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
let offset = self.previous_start_of_word();
|
|
||||||
self.move_to(offset, None, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn move_to_next_word(
|
|
||||||
&mut self,
|
|
||||||
_: &MoveToNextWord,
|
|
||||||
_: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
let offset = self.next_end_of_word();
|
|
||||||
self.move_to(offset, None, cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,456 +0,0 @@
|
|||||||
use std::ops::Range;
|
|
||||||
|
|
||||||
use ropey::{LineType, Rope, RopeSlice};
|
|
||||||
use sum_tree::Bias;
|
|
||||||
#[cfg(not(target_family = "wasm"))]
|
|
||||||
pub use tree_sitter::{InputEdit, Point};
|
|
||||||
|
|
||||||
#[cfg(target_family = "wasm")]
|
|
||||||
/// Stub type for tree-sitter Point on WASM (tree-sitter not available).
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
||||||
pub struct Point {
|
|
||||||
pub row: usize,
|
|
||||||
pub column: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_family = "wasm")]
|
|
||||||
impl Point {
|
|
||||||
pub fn new(row: usize, column: usize) -> Self {
|
|
||||||
Self { row, column }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_family = "wasm")]
|
|
||||||
/// Stub type for tree-sitter InputEdit on WASM (tree-sitter not available).
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub struct InputEdit {
|
|
||||||
pub start_byte: usize,
|
|
||||||
pub old_end_byte: usize,
|
|
||||||
pub new_end_byte: usize,
|
|
||||||
pub start_position: Point,
|
|
||||||
pub old_end_position: Point,
|
|
||||||
pub new_end_position: Point,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type Position = lsp_types::Position;
|
|
||||||
|
|
||||||
/// An iterator over the lines of a `Rope`.
|
|
||||||
pub struct RopeLines<'a> {
|
|
||||||
rope: &'a Rope,
|
|
||||||
row: usize,
|
|
||||||
end_row: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> RopeLines<'a> {
|
|
||||||
/// Create a new `RopeLines` iterator.
|
|
||||||
pub fn new(rope: &'a Rope) -> Self {
|
|
||||||
let end_row = rope.lines_len();
|
|
||||||
Self {
|
|
||||||
row: 0,
|
|
||||||
end_row,
|
|
||||||
rope,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<'a> Iterator for RopeLines<'a> {
|
|
||||||
type Item = RopeSlice<'a>;
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
|
||||||
if self.row >= self.end_row {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let line = self.rope.slice_line(self.row);
|
|
||||||
self.row += 1;
|
|
||||||
Some(line)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn nth(&mut self, n: usize) -> Option<Self::Item> {
|
|
||||||
self.row = self.row.saturating_add(n);
|
|
||||||
self.next()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
|
||||||
let len = self.end_row - self.row;
|
|
||||||
(len, Some(len))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::iter::ExactSizeIterator for RopeLines<'_> {}
|
|
||||||
impl std::iter::FusedIterator for RopeLines<'_> {}
|
|
||||||
|
|
||||||
/// An extension trait for [`Rope`] to provide additional utility methods.
|
|
||||||
pub trait RopeExt {
|
|
||||||
/// Start offset of the line at the given row (0-based) index.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use gpui_component::{Rope, RopeExt};
|
|
||||||
///
|
|
||||||
/// let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope");
|
|
||||||
/// assert_eq!(rope.line_start_offset(0), 0);
|
|
||||||
/// assert_eq!(rope.line_start_offset(1), 6);
|
|
||||||
/// ```
|
|
||||||
fn line_start_offset(&self, row: usize) -> usize;
|
|
||||||
|
|
||||||
/// Line the end offset (including `\n`) of the line at the given row (0-based) index.
|
|
||||||
///
|
|
||||||
/// Return the end of the rope if the row is out of bounds.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use gpui_component::{Rope, RopeExt};
|
|
||||||
/// let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope");
|
|
||||||
/// assert_eq!(rope.line_end_offset(0), 5); // "Hello\n"
|
|
||||||
/// assert_eq!(rope.line_end_offset(1), 12); // "World\r\n"
|
|
||||||
/// ```
|
|
||||||
fn line_end_offset(&self, row: usize) -> usize;
|
|
||||||
|
|
||||||
/// Return a line slice at the given row (0-based) index. including `\r` if present, but not `\n`.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use gpui_component::{Rope, RopeExt};
|
|
||||||
/// let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope");
|
|
||||||
/// assert_eq!(rope.slice_line(0).to_string(), "Hello");
|
|
||||||
/// assert_eq!(rope.slice_line(1).to_string(), "World\r");
|
|
||||||
/// assert_eq!(rope.slice_line(2).to_string(), "This is a test 中文");
|
|
||||||
/// assert_eq!(rope.slice_line(6).to_string(), ""); // out of bounds
|
|
||||||
/// ```
|
|
||||||
fn slice_line(&self, row: usize) -> RopeSlice<'_>;
|
|
||||||
|
|
||||||
/// Return a slice of rows in the given range (0-based, end exclusive).
|
|
||||||
///
|
|
||||||
/// If the range is out of bounds, it will be clamped to the valid range.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use gpui_component::{Rope, RopeExt};
|
|
||||||
/// let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope");
|
|
||||||
/// assert_eq!(rope.slice_lines(0..2).to_string(), "Hello\nWorld\r");
|
|
||||||
/// assert_eq!(rope.slice_lines(1..3).to_string(), "World\r\nThis is a test 中文");
|
|
||||||
/// assert_eq!(rope.slice_lines(2..5).to_string(), "This is a test 中文\nRope");
|
|
||||||
/// assert_eq!(rope.slice_lines(3..10).to_string(), "Rope");
|
|
||||||
/// assert_eq!(rope.slice_lines(5..10).to_string(), ""); // out of bounds
|
|
||||||
/// ```
|
|
||||||
fn slice_lines(&self, rows_range: Range<usize>) -> RopeSlice<'_>;
|
|
||||||
|
|
||||||
/// Return an iterator over all lines in the rope.
|
|
||||||
///
|
|
||||||
/// Each line slice includes `\r` if present, but not `\n`.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use gpui_component::{Rope, RopeExt};
|
|
||||||
/// let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope");
|
|
||||||
/// let lines: Vec<_> = rope.iter_lines().map(|r| r.to_string()).collect();
|
|
||||||
/// assert_eq!(lines, vec!["Hello", "World\r", "This is a test 中文", "Rope"]);
|
|
||||||
/// ```
|
|
||||||
fn iter_lines(&self) -> RopeLines<'_>;
|
|
||||||
|
|
||||||
/// Return the number of lines in the rope.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use gpui_component::{Rope, RopeExt};
|
|
||||||
/// let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope");
|
|
||||||
/// assert_eq!(rope.lines_len(), 4);
|
|
||||||
/// ```
|
|
||||||
fn lines_len(&self) -> usize;
|
|
||||||
|
|
||||||
/// Return the length of the row (0-based) in characters, including `\r` if present, but not `\n`.
|
|
||||||
///
|
|
||||||
/// If the row is out of bounds, return 0.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use gpui_component::{Rope, RopeExt};
|
|
||||||
/// let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope");
|
|
||||||
/// assert_eq!(rope.line_len(0), 5); // "Hello"
|
|
||||||
/// assert_eq!(rope.line_len(1), 6); // "World\r"
|
|
||||||
/// assert_eq!(rope.line_len(2), 21); // "This is a test 中文"
|
|
||||||
/// assert_eq!(rope.line_len(4), 0); // out of bounds
|
|
||||||
/// ```
|
|
||||||
fn line_len(&self, row: usize) -> usize;
|
|
||||||
|
|
||||||
/// Replace the text in the given byte range with new text.
|
|
||||||
///
|
|
||||||
/// # Panics
|
|
||||||
///
|
|
||||||
/// - If the range is not on char boundary.
|
|
||||||
/// - If the range is out of bounds.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use gpui_component::{Rope, RopeExt};
|
|
||||||
/// let mut rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope");
|
|
||||||
/// rope.replace(6..11, "Universe");
|
|
||||||
/// assert_eq!(rope.to_string(), "Hello\nUniverse\r\nThis is a test 中文\nRope");
|
|
||||||
/// ```
|
|
||||||
fn replace(&mut self, range: Range<usize>, new_text: &str);
|
|
||||||
|
|
||||||
/// Get char at the given offset (byte).
|
|
||||||
///
|
|
||||||
/// - If the offset is in the middle of a multi-byte character will panic.
|
|
||||||
/// - If the offset is out of bounds, return None.
|
|
||||||
fn char_at(&self, offset: usize) -> Option<char>;
|
|
||||||
|
|
||||||
/// Get the byte offset from the given line, column [`Position`] (0-based).
|
|
||||||
///
|
|
||||||
/// The column is in characters.
|
|
||||||
fn position_to_offset(&self, line_col: &Position) -> usize;
|
|
||||||
|
|
||||||
/// Get the line, column [`Position`] (0-based) from the given byte offset.
|
|
||||||
///
|
|
||||||
/// The column is in characters.
|
|
||||||
fn offset_to_position(&self, offset: usize) -> Position;
|
|
||||||
|
|
||||||
/// Get point (row, column) from the given byte offset.
|
|
||||||
///
|
|
||||||
/// The column is in bytes.
|
|
||||||
fn offset_to_point(&self, offset: usize) -> Point;
|
|
||||||
|
|
||||||
/// Get byte offset from the given point (row, column).
|
|
||||||
///
|
|
||||||
/// The column is 0-based in bytes.
|
|
||||||
fn point_to_offset(&self, point: Point) -> usize;
|
|
||||||
|
|
||||||
/// Get the word byte range at the given byte offset (0-based).
|
|
||||||
fn word_range(&self, offset: usize) -> Option<Range<usize>>;
|
|
||||||
|
|
||||||
/// Get word at the given byte offset (0-based).
|
|
||||||
fn word_at(&self, offset: usize) -> String;
|
|
||||||
|
|
||||||
/// Convert offset in UTF-16 to byte offset (0-based).
|
|
||||||
///
|
|
||||||
/// Runs in O(log N) time.
|
|
||||||
fn offset_utf16_to_offset(&self, offset_utf16: usize) -> usize;
|
|
||||||
|
|
||||||
/// Convert byte offset (0-based) to offset in UTF-16.
|
|
||||||
///
|
|
||||||
/// Runs in O(log N) time.
|
|
||||||
fn offset_to_offset_utf16(&self, offset: usize) -> usize;
|
|
||||||
|
|
||||||
/// Get a clipped offset (avoid in a char boundary).
|
|
||||||
///
|
|
||||||
/// - If Bias::Left and inside the char boundary, return the ix - 1;
|
|
||||||
/// - If Bias::Right and in inside char boundary, return the ix + 1;
|
|
||||||
/// - Otherwise return the ix.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use gpui_component::{Rope, RopeExt};
|
|
||||||
/// use sum_tree::Bias;
|
|
||||||
///
|
|
||||||
/// let rope = Rope::from("Hello 中文🎉 test\nRope");
|
|
||||||
/// assert_eq!(rope.clip_offset(5, Bias::Left), 5);
|
|
||||||
/// // Inside multi-byte character '中' (3 bytes)
|
|
||||||
/// assert_eq!(rope.clip_offset(7, Bias::Left), 6);
|
|
||||||
/// assert_eq!(rope.clip_offset(7, Bias::Right), 9);
|
|
||||||
/// ```
|
|
||||||
fn clip_offset(&self, offset: usize, bias: Bias) -> usize;
|
|
||||||
|
|
||||||
/// Convert offset in characters to byte offset (0-based).
|
|
||||||
///
|
|
||||||
/// Run in O(n) time.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use gpui_component::{Rope, RopeExt};
|
|
||||||
/// let rope = Rope::from("a 中文🎉 test\nRope");
|
|
||||||
/// assert_eq!(rope.char_index_to_offset(0), 0);
|
|
||||||
/// assert_eq!(rope.char_index_to_offset(1), 1);
|
|
||||||
/// assert_eq!(rope.char_index_to_offset(3), "a 中".len());
|
|
||||||
/// assert_eq!(rope.char_index_to_offset(5), "a 中文🎉".len());
|
|
||||||
/// ```
|
|
||||||
fn char_index_to_offset(&self, char_index: usize) -> usize;
|
|
||||||
|
|
||||||
/// Convert byte offset (0-based) to offset in characters.
|
|
||||||
///
|
|
||||||
/// Run in O(n) time.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use gpui_component::{Rope, RopeExt};
|
|
||||||
/// let rope = Rope::from("a 中文🎉 test\nRope");
|
|
||||||
/// assert_eq!(rope.offset_to_char_index(0), 0);
|
|
||||||
/// assert_eq!(rope.offset_to_char_index(1), 1);
|
|
||||||
/// assert_eq!(rope.offset_to_char_index(3), 3);
|
|
||||||
/// assert_eq!(rope.offset_to_char_index(4), 3);
|
|
||||||
/// ```
|
|
||||||
fn offset_to_char_index(&self, offset: usize) -> usize;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RopeExt for Rope {
|
|
||||||
fn slice_line(&self, row: usize) -> RopeSlice<'_> {
|
|
||||||
let total_lines = self.lines_len();
|
|
||||||
if row >= total_lines {
|
|
||||||
return self.slice(0..0);
|
|
||||||
}
|
|
||||||
|
|
||||||
let line = self.line(row, LineType::LF);
|
|
||||||
if line.len() > 0 {
|
|
||||||
let line_end = line.len() - 1;
|
|
||||||
if line.is_char_boundary(line_end) && line.char(line_end) == '\n' {
|
|
||||||
return line.slice(..line_end);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
line
|
|
||||||
}
|
|
||||||
|
|
||||||
fn slice_lines(&self, rows_range: Range<usize>) -> RopeSlice<'_> {
|
|
||||||
let start = self.line_start_offset(rows_range.start);
|
|
||||||
let end = self.line_end_offset(rows_range.end.saturating_sub(1));
|
|
||||||
self.slice(start..end)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn iter_lines(&self) -> RopeLines<'_> {
|
|
||||||
RopeLines::new(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn line_len(&self, row: usize) -> usize {
|
|
||||||
self.slice_line(row).len()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn line_start_offset(&self, row: usize) -> usize {
|
|
||||||
self.point_to_offset(Point::new(row, 0))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn offset_to_point(&self, offset: usize) -> Point {
|
|
||||||
let offset = self.clip_offset(offset, Bias::Left);
|
|
||||||
let row = self.byte_to_line_idx(offset, LineType::LF);
|
|
||||||
let line_start = self.line_to_byte_idx(row, LineType::LF);
|
|
||||||
let column = offset.saturating_sub(line_start);
|
|
||||||
Point::new(row, column)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn point_to_offset(&self, point: Point) -> usize {
|
|
||||||
if point.row >= self.lines_len() {
|
|
||||||
return self.len();
|
|
||||||
}
|
|
||||||
|
|
||||||
let line_start = self.line_to_byte_idx(point.row, LineType::LF);
|
|
||||||
line_start + point.column
|
|
||||||
}
|
|
||||||
|
|
||||||
fn position_to_offset(&self, pos: &Position) -> usize {
|
|
||||||
let line = self.slice_line(pos.line as usize);
|
|
||||||
self.line_start_offset(pos.line as usize)
|
|
||||||
+ line
|
|
||||||
.chars()
|
|
||||||
.take(pos.character as usize)
|
|
||||||
.map(|c| c.len_utf8())
|
|
||||||
.sum::<usize>()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn offset_to_position(&self, offset: usize) -> Position {
|
|
||||||
let point = self.offset_to_point(offset);
|
|
||||||
let line = self.slice_line(point.row);
|
|
||||||
let offset = line.utf16_to_byte_idx(line.byte_to_utf16_idx(point.column));
|
|
||||||
let character = line.slice(..offset).chars().count();
|
|
||||||
Position::new(point.row as u32, character as u32)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn line_end_offset(&self, row: usize) -> usize {
|
|
||||||
if row > self.lines_len() {
|
|
||||||
return self.len();
|
|
||||||
}
|
|
||||||
|
|
||||||
self.line_start_offset(row) + self.line_len(row)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn lines_len(&self) -> usize {
|
|
||||||
self.len_lines(LineType::LF)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn char_at(&self, offset: usize) -> Option<char> {
|
|
||||||
if offset > self.len() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.get_char(offset).ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn word_range(&self, offset: usize) -> Option<Range<usize>> {
|
|
||||||
if offset >= self.len() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut left = String::new();
|
|
||||||
let offset = self.clip_offset(offset, Bias::Left);
|
|
||||||
for c in self.chars_at(offset).reversed() {
|
|
||||||
if c.is_alphanumeric() || c == '_' {
|
|
||||||
left.insert(0, c);
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let start = offset.saturating_sub(left.len());
|
|
||||||
|
|
||||||
let right = self
|
|
||||||
.chars_at(offset)
|
|
||||||
.take_while(|c| c.is_alphanumeric() || *c == '_')
|
|
||||||
.collect::<String>();
|
|
||||||
|
|
||||||
let end = offset + right.len();
|
|
||||||
|
|
||||||
if start == end { None } else { Some(start..end) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn word_at(&self, offset: usize) -> String {
|
|
||||||
if let Some(range) = self.word_range(offset) {
|
|
||||||
self.slice(range).to_string()
|
|
||||||
} else {
|
|
||||||
String::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn offset_utf16_to_offset(&self, offset_utf16: usize) -> usize {
|
|
||||||
if offset_utf16 > self.len_utf16() {
|
|
||||||
return self.len();
|
|
||||||
}
|
|
||||||
|
|
||||||
self.utf16_to_byte_idx(offset_utf16)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn offset_to_offset_utf16(&self, offset: usize) -> usize {
|
|
||||||
if offset > self.len() {
|
|
||||||
return self.len_utf16();
|
|
||||||
}
|
|
||||||
|
|
||||||
self.byte_to_utf16_idx(offset)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn replace(&mut self, range: Range<usize>, new_text: &str) {
|
|
||||||
let range =
|
|
||||||
self.clip_offset(range.start, Bias::Left)..self.clip_offset(range.end, Bias::Right);
|
|
||||||
self.remove(range.clone());
|
|
||||||
self.insert(range.start, new_text);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn clip_offset(&self, offset: usize, bias: Bias) -> usize {
|
|
||||||
if offset > self.len() {
|
|
||||||
return self.len();
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.is_char_boundary(offset) {
|
|
||||||
return offset;
|
|
||||||
}
|
|
||||||
|
|
||||||
if bias == Bias::Left {
|
|
||||||
self.floor_char_boundary(offset)
|
|
||||||
} else {
|
|
||||||
self.ceil_char_boundary(offset)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn char_index_to_offset(&self, char_offset: usize) -> usize {
|
|
||||||
self.chars().take(char_offset).map(|c| c.len_utf8()).sum()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn offset_to_char_index(&self, offset: usize) -> usize {
|
|
||||||
let offset = self.clip_offset(offset, Bias::Right);
|
|
||||||
self.slice(..offset).chars().count()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
use std::ops::Range;
|
|
||||||
|
|
||||||
use gpui::{Context, Window};
|
|
||||||
use ropey::Rope;
|
|
||||||
use sum_tree::Bias;
|
|
||||||
|
|
||||||
use crate::input::{InputState, RopeExt};
|
|
||||||
|
|
||||||
impl InputState {
|
|
||||||
/// Select the word at the given offset on double-click.
|
|
||||||
///
|
|
||||||
/// The offset is the UTF-8 offset.
|
|
||||||
pub(super) fn select_word(&mut self, offset: usize, _: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
let Some(range) = TextSelector::word_range(&self.text, offset) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
self.selected_range = (range.start..range.end).into();
|
|
||||||
self.selected_word_range = Some(self.selected_range);
|
|
||||||
cx.notify()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Select the line at the given offset on triple-click.
|
|
||||||
///
|
|
||||||
/// The offset is the UTF-8 offset.
|
|
||||||
pub(super) fn select_line(&mut self, offset: usize, _: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
let range = TextSelector::line_range(&self.text, offset);
|
|
||||||
self.selected_range = (range.start..range.end).into();
|
|
||||||
self.selected_word_range = None;
|
|
||||||
cx.notify()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct TextSelector;
|
|
||||||
impl TextSelector {
|
|
||||||
/// Select a line in the given text at the specified offset.
|
|
||||||
///
|
|
||||||
/// The offset is the UTF-8 offset.
|
|
||||||
///
|
|
||||||
/// Returns the start and end offsets of the selected line.
|
|
||||||
pub fn line_range(text: &Rope, offset: usize) -> Range<usize> {
|
|
||||||
let offset = text.clip_offset(offset, Bias::Left);
|
|
||||||
let row = text.offset_to_point(offset).row;
|
|
||||||
let start = text.line_start_offset(row);
|
|
||||||
let end = text.line_end_offset(row);
|
|
||||||
|
|
||||||
start..end
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Select a word in the given text at the specified offset.
|
|
||||||
///
|
|
||||||
/// The offset is the UTF-8 offset.
|
|
||||||
///
|
|
||||||
/// Returns the start and end offsets of the selected word.
|
|
||||||
pub fn word_range(text: &Rope, offset: usize) -> Option<Range<usize>> {
|
|
||||||
let offset = text.clip_offset(offset, Bias::Left);
|
|
||||||
let char = text.char_at(offset)?;
|
|
||||||
let end = offset + char.len_utf8();
|
|
||||||
let prev_chars = text.chars_at(offset).reversed().take(128);
|
|
||||||
let next_chars = text.chars_at(end).take(128);
|
|
||||||
|
|
||||||
Some(word_range_from_chars(offset, char, prev_chars, next_chars))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub(crate) enum CharType {
|
|
||||||
/// a-z, A-Z, 0-9, _
|
|
||||||
Word,
|
|
||||||
/// '\t', ' ', '\u{00A0}' etc.
|
|
||||||
Whitespace,
|
|
||||||
/// \n, \r
|
|
||||||
Newline,
|
|
||||||
/// . , ; : ( ) [ ] { } ... or CJK characters: `汉`, `🎉` etc.
|
|
||||||
Other,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<char> for CharType {
|
|
||||||
fn from(c: char) -> Self {
|
|
||||||
match c {
|
|
||||||
c if is_word_char(c) => CharType::Word,
|
|
||||||
c if c == '\n' || c == '\r' => CharType::Newline,
|
|
||||||
c if c.is_whitespace() => CharType::Whitespace,
|
|
||||||
_ => CharType::Other,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CharType {
|
|
||||||
fn is_connectable(self, c: char) -> bool {
|
|
||||||
matches!(
|
|
||||||
(self, CharType::from(c)),
|
|
||||||
(CharType::Word, CharType::Word) | (CharType::Whitespace, CharType::Whitespace)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_word_char(c: char) -> bool {
|
|
||||||
matches!(c, '_')
|
|
||||||
// ASCII alphanumeric characters, for English, numbers: `Hello123`, etc.
|
|
||||||
|| c.is_ascii_alphanumeric()
|
|
||||||
// Latin script in Unicode for French, German, Spanish, etc.
|
|
||||||
|| matches!(c, '\u{00C0}'..='\u{00FF}')
|
|
||||||
|| matches!(c, '\u{0100}'..='\u{017F}')
|
|
||||||
|| matches!(c, '\u{0180}'..='\u{024F}')
|
|
||||||
// Cyrillic for Russian, Ukrainian, etc.
|
|
||||||
|| matches!(c, '\u{0400}'..='\u{04FF}')
|
|
||||||
// Vietnamese
|
|
||||||
|| matches!(c, '\u{1E00}'..='\u{1EFF}')
|
|
||||||
|| matches!(c, '\u{0300}'..='\u{036F}')
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn word_range_from_chars(
|
|
||||||
offset: usize,
|
|
||||||
c: char,
|
|
||||||
prev_chars: impl Iterator<Item = char>,
|
|
||||||
next_chars: impl Iterator<Item = char>,
|
|
||||||
) -> Range<usize> {
|
|
||||||
let char_type = CharType::from(c);
|
|
||||||
let mut start = offset;
|
|
||||||
let mut end = offset + c.len_utf8();
|
|
||||||
|
|
||||||
for prev in prev_chars.take(128) {
|
|
||||||
if char_type.is_connectable(prev) {
|
|
||||||
start -= prev.len_utf8();
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for next in next_chars.take(128) {
|
|
||||||
if char_type.is_connectable(next) {
|
|
||||||
end += next.len_utf8();
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
start..end
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
+3
-16
@@ -1,8 +1,5 @@
|
|||||||
pub use element_ext::ElementExt;
|
pub use gpui_base::{ElementExt, IndexPath, InteractiveElementExt};
|
||||||
pub use event::InteractiveElementExt;
|
|
||||||
pub use focusable::FocusableCycle;
|
|
||||||
pub use icon::*;
|
pub use icon::*;
|
||||||
pub use index_path::IndexPath;
|
|
||||||
pub use kbd::*;
|
pub use kbd::*;
|
||||||
pub use root::{Root, window_paddings};
|
pub use root::{Root, window_paddings};
|
||||||
pub use styled::*;
|
pub use styled::*;
|
||||||
@@ -11,18 +8,14 @@ pub use window_ext::*;
|
|||||||
|
|
||||||
pub use crate::Disableable;
|
pub use crate::Disableable;
|
||||||
|
|
||||||
pub mod actions;
|
|
||||||
pub mod animation;
|
pub mod animation;
|
||||||
pub mod avatar;
|
pub mod avatar;
|
||||||
pub mod button;
|
pub mod button;
|
||||||
pub mod checkbox;
|
|
||||||
pub mod divider;
|
pub mod divider;
|
||||||
pub mod dock;
|
pub mod dock;
|
||||||
pub mod group_box;
|
pub mod group_box;
|
||||||
pub mod history;
|
|
||||||
pub mod indicator;
|
pub mod indicator;
|
||||||
pub mod input;
|
pub mod input;
|
||||||
pub mod list;
|
|
||||||
pub mod menu;
|
pub mod menu;
|
||||||
pub mod modal;
|
pub mod modal;
|
||||||
pub mod notification;
|
pub mod notification;
|
||||||
@@ -34,11 +27,7 @@ pub mod switch;
|
|||||||
pub mod tab;
|
pub mod tab;
|
||||||
pub mod tooltip;
|
pub mod tooltip;
|
||||||
|
|
||||||
mod element_ext;
|
|
||||||
mod event;
|
|
||||||
mod focusable;
|
|
||||||
mod icon;
|
mod icon;
|
||||||
mod index_path;
|
|
||||||
mod kbd;
|
mod kbd;
|
||||||
mod root;
|
mod root;
|
||||||
mod styled;
|
mod styled;
|
||||||
@@ -50,9 +39,7 @@ mod window_ext;
|
|||||||
/// This must be called before using any of the UI components.
|
/// This must be called before using any of the UI components.
|
||||||
/// You can initialize the UI module at your application's entry point.
|
/// You can initialize the UI module at your application's entry point.
|
||||||
pub fn init(cx: &mut gpui::App) {
|
pub fn init(cx: &mut gpui::App) {
|
||||||
input::init(cx);
|
gpui_base::init(cx);
|
||||||
list::init(cx);
|
theme::sync_base(cx);
|
||||||
modal::init(cx);
|
|
||||||
popover::init(cx);
|
|
||||||
menu::init(cx);
|
menu::init(cx);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,221 +0,0 @@
|
|||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
use gpui::{App, Pixels, Size};
|
|
||||||
|
|
||||||
use crate::IndexPath;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub(crate) enum RowEntry {
|
|
||||||
Entry(IndexPath),
|
|
||||||
SectionHeader(usize),
|
|
||||||
SectionFooter(usize),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
|
||||||
pub(crate) struct MeasuredEntrySize {
|
|
||||||
pub(crate) item_size: Size<Pixels>,
|
|
||||||
pub(crate) section_header_size: Size<Pixels>,
|
|
||||||
pub(crate) section_footer_size: Size<Pixels>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RowEntry {
|
|
||||||
#[inline]
|
|
||||||
#[allow(unused)]
|
|
||||||
pub(crate) fn is_section_header(&self) -> bool {
|
|
||||||
matches!(self, RowEntry::SectionHeader(_))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn eq_index_path(&self, path: &IndexPath) -> bool {
|
|
||||||
match self {
|
|
||||||
RowEntry::Entry(index_path) => index_path == path,
|
|
||||||
RowEntry::SectionHeader(_) | RowEntry::SectionFooter(_) => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(unused)]
|
|
||||||
pub(crate) fn index(&self) -> IndexPath {
|
|
||||||
match self {
|
|
||||||
RowEntry::Entry(index_path) => *index_path,
|
|
||||||
RowEntry::SectionHeader(ix) => IndexPath::default().section(*ix),
|
|
||||||
RowEntry::SectionFooter(ix) => IndexPath::default().section(*ix),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
#[allow(unused)]
|
|
||||||
pub(crate) fn is_section_footer(&self) -> bool {
|
|
||||||
matches!(self, RowEntry::SectionFooter(_))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn is_entry(&self) -> bool {
|
|
||||||
matches!(self, RowEntry::Entry(_))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
#[allow(unused)]
|
|
||||||
pub(crate) fn section_ix(&self) -> Option<usize> {
|
|
||||||
match self {
|
|
||||||
RowEntry::SectionHeader(ix) | RowEntry::SectionFooter(ix) => Some(*ix),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Clone)]
|
|
||||||
pub(crate) struct RowsCache {
|
|
||||||
/// Only have section's that have rows.
|
|
||||||
pub(crate) entities: Rc<Vec<RowEntry>>,
|
|
||||||
pub(crate) items_count: usize,
|
|
||||||
/// The sections, the item is number of rows in each section.
|
|
||||||
pub(crate) sections: Rc<Vec<usize>>,
|
|
||||||
pub(crate) entries_sizes: Rc<Vec<Size<Pixels>>>,
|
|
||||||
measured_size: MeasuredEntrySize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RowsCache {
|
|
||||||
pub(crate) fn get(&self, flatten_ix: usize) -> Option<RowEntry> {
|
|
||||||
self.entities.get(flatten_ix).cloned()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the number of flattened rows (Includes header, item, footer).
|
|
||||||
pub(crate) fn len(&self) -> usize {
|
|
||||||
self.entities.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return the number of items in the cache.
|
|
||||||
pub(crate) fn items_count(&self) -> usize {
|
|
||||||
self.items_count
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the index of the Entry with given path in the flattened rows.
|
|
||||||
pub(crate) fn position_of(&self, path: &IndexPath) -> Option<usize> {
|
|
||||||
self.entities
|
|
||||||
.iter()
|
|
||||||
.position(|p| p.is_entry() && p.eq_index_path(path))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return prev row, if the row is the first in the first section, goes to the last row.
|
|
||||||
///
|
|
||||||
/// Empty rows section are skipped.
|
|
||||||
pub(crate) fn prev(&self, path: Option<IndexPath>) -> IndexPath {
|
|
||||||
let path = path.unwrap_or_default();
|
|
||||||
let Some(pos) = self.position_of(&path) else {
|
|
||||||
return self
|
|
||||||
.entities
|
|
||||||
.iter()
|
|
||||||
.rfind(|entry| entry.is_entry())
|
|
||||||
.map(|entry| entry.index())
|
|
||||||
.unwrap_or_default();
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(path) = self
|
|
||||||
.entities
|
|
||||||
.iter()
|
|
||||||
.take(pos)
|
|
||||||
.rev()
|
|
||||||
.find(|entry| entry.is_entry())
|
|
||||||
.map(|entry| entry.index())
|
|
||||||
{
|
|
||||||
path
|
|
||||||
} else {
|
|
||||||
self.entities
|
|
||||||
.iter()
|
|
||||||
.rfind(|entry| entry.is_entry())
|
|
||||||
.map(|entry| entry.index())
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the next row, if the row is the last in the last section, goes to the first row.
|
|
||||||
///
|
|
||||||
/// Empty rows section are skipped.
|
|
||||||
pub(crate) fn next(&self, path: Option<IndexPath>) -> IndexPath {
|
|
||||||
let Some(mut path) = path else {
|
|
||||||
return IndexPath::default();
|
|
||||||
};
|
|
||||||
|
|
||||||
let Some(pos) = self.position_of(&path) else {
|
|
||||||
return self
|
|
||||||
.entities
|
|
||||||
.iter()
|
|
||||||
.find(|entry| entry.is_entry())
|
|
||||||
.map(|entry| entry.index())
|
|
||||||
.unwrap_or_default();
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(next_path) = self
|
|
||||||
.entities
|
|
||||||
.iter()
|
|
||||||
.skip(pos + 1)
|
|
||||||
.find(|entry| entry.is_entry())
|
|
||||||
.map(|entry| entry.index())
|
|
||||||
{
|
|
||||||
path = next_path;
|
|
||||||
} else {
|
|
||||||
path = self
|
|
||||||
.entities
|
|
||||||
.iter()
|
|
||||||
.find(|entry| entry.is_entry())
|
|
||||||
.map(|entry| entry.index())
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
path
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn prepare_if_needed<F>(
|
|
||||||
&mut self,
|
|
||||||
sections_count: usize,
|
|
||||||
measured_size: MeasuredEntrySize,
|
|
||||||
cx: &App,
|
|
||||||
rows_count_f: F,
|
|
||||||
) where
|
|
||||||
F: Fn(usize, &App) -> usize,
|
|
||||||
{
|
|
||||||
let mut new_sections = vec![];
|
|
||||||
for section_ix in 0..sections_count {
|
|
||||||
new_sections.push(rows_count_f(section_ix, cx));
|
|
||||||
}
|
|
||||||
|
|
||||||
let need_update = new_sections != *self.sections || self.measured_size != measured_size;
|
|
||||||
|
|
||||||
if !need_update {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut entries_sizes = vec![];
|
|
||||||
let mut total_items_count = 0;
|
|
||||||
self.measured_size = measured_size;
|
|
||||||
self.sections = Rc::new(new_sections);
|
|
||||||
self.entities = Rc::new(
|
|
||||||
self.sections
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.flat_map(|(section, items_count)| {
|
|
||||||
total_items_count += items_count;
|
|
||||||
let mut children = vec![];
|
|
||||||
if *items_count == 0 {
|
|
||||||
return children;
|
|
||||||
}
|
|
||||||
|
|
||||||
children.push(RowEntry::SectionHeader(section));
|
|
||||||
entries_sizes.push(measured_size.section_header_size);
|
|
||||||
for row in 0..*items_count {
|
|
||||||
children.push(RowEntry::Entry(IndexPath {
|
|
||||||
section,
|
|
||||||
row,
|
|
||||||
..Default::default()
|
|
||||||
}));
|
|
||||||
entries_sizes.push(measured_size.item_size);
|
|
||||||
}
|
|
||||||
children.push(RowEntry::SectionFooter(section));
|
|
||||||
entries_sizes.push(measured_size.section_footer_size);
|
|
||||||
children
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
);
|
|
||||||
self.entries_sizes = Rc::new(entries_sizes);
|
|
||||||
self.items_count = total_items_count;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,171 +0,0 @@
|
|||||||
use gpui::{AnyElement, App, Context, IntoElement, ParentElement as _, Styled as _, Task, Window};
|
|
||||||
use theme::ActiveTheme;
|
|
||||||
|
|
||||||
use crate::list::loading::Loading;
|
|
||||||
use crate::list::ListState;
|
|
||||||
use crate::{h_flex, Icon, IconName, IndexPath, Selectable};
|
|
||||||
|
|
||||||
/// A delegate for the List.
|
|
||||||
#[allow(unused)]
|
|
||||||
pub trait ListDelegate: Sized + 'static {
|
|
||||||
type Item: Selectable + IntoElement;
|
|
||||||
|
|
||||||
/// When Query Input change, this method will be called.
|
|
||||||
/// You can perform search here.
|
|
||||||
fn perform_search(
|
|
||||||
&mut self,
|
|
||||||
query: &str,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<ListState<Self>>,
|
|
||||||
) -> Task<()> {
|
|
||||||
Task::ready(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return the number of sections in the list, default is 1.
|
|
||||||
///
|
|
||||||
/// Min value is 1.
|
|
||||||
fn sections_count(&self, cx: &App) -> usize {
|
|
||||||
1
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return the number of items in the section at the given index.
|
|
||||||
///
|
|
||||||
/// NOTE: Only the sections with items_count > 0 will be rendered. If the section has 0 items,
|
|
||||||
/// the section header and footer will also be skipped.
|
|
||||||
fn items_count(&self, section: usize, cx: &App) -> usize;
|
|
||||||
|
|
||||||
/// Render the item at the given index.
|
|
||||||
///
|
|
||||||
/// Return None will skip the item.
|
|
||||||
///
|
|
||||||
/// NOTE: Every item should have same height.
|
|
||||||
fn render_item(
|
|
||||||
&mut self,
|
|
||||||
ix: IndexPath,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<ListState<Self>>,
|
|
||||||
) -> Option<Self::Item>;
|
|
||||||
|
|
||||||
/// Render the section header at the given index, default is None.
|
|
||||||
///
|
|
||||||
/// NOTE: Every header should have same height.
|
|
||||||
fn render_section_header(
|
|
||||||
&mut self,
|
|
||||||
section: usize,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<ListState<Self>>,
|
|
||||||
) -> Option<impl IntoElement> {
|
|
||||||
None::<AnyElement>
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Render the section footer at the given index, default is None.
|
|
||||||
///
|
|
||||||
/// NOTE: Every footer should have same height.
|
|
||||||
fn render_section_footer(
|
|
||||||
&mut self,
|
|
||||||
section: usize,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<ListState<Self>>,
|
|
||||||
) -> Option<impl IntoElement> {
|
|
||||||
None::<AnyElement>
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return a Element to show when list is empty.
|
|
||||||
fn render_empty(
|
|
||||||
&mut self,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<ListState<Self>>,
|
|
||||||
) -> impl IntoElement {
|
|
||||||
h_flex()
|
|
||||||
.size_full()
|
|
||||||
.justify_center()
|
|
||||||
.text_color(cx.theme().text_muted.opacity(0.6))
|
|
||||||
.child(Icon::new(IconName::Inbox).size_12())
|
|
||||||
.into_any_element()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns Some(AnyElement) to render the initial state of the list.
|
|
||||||
///
|
|
||||||
/// This can be used to show a view for the list before the user has
|
|
||||||
/// interacted with it.
|
|
||||||
///
|
|
||||||
/// For example: The last search results, or the last selected item.
|
|
||||||
///
|
|
||||||
/// Default is None, that means no initial state.
|
|
||||||
fn render_initial(
|
|
||||||
&mut self,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<ListState<Self>>,
|
|
||||||
) -> Option<AnyElement> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the loading state to show the loading view.
|
|
||||||
fn loading(&self, cx: &App) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a Element to show when loading, default is built-in Skeleton
|
|
||||||
/// loading view.
|
|
||||||
fn render_loading(
|
|
||||||
&mut self,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<ListState<Self>>,
|
|
||||||
) -> impl IntoElement {
|
|
||||||
Loading
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the selected index, just store the ix, don't confirm.
|
|
||||||
fn set_selected_index(
|
|
||||||
&mut self,
|
|
||||||
ix: Option<IndexPath>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<ListState<Self>>,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Set the index of the item that has been right clicked.
|
|
||||||
fn set_right_clicked_index(
|
|
||||||
&mut self,
|
|
||||||
ix: Option<IndexPath>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<ListState<Self>>,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the confirm and give the selected index,
|
|
||||||
/// this is means user have clicked the item or pressed Enter.
|
|
||||||
///
|
|
||||||
/// This will always to `set_selected_index` before confirm.
|
|
||||||
fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context<ListState<Self>>) {
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Cancel the selection, e.g.: Pressed ESC.
|
|
||||||
fn cancel(&mut self, window: &mut Window, cx: &mut Context<ListState<Self>>) {}
|
|
||||||
|
|
||||||
/// Return true to enable load more data when scrolling to the bottom.
|
|
||||||
///
|
|
||||||
/// Default: false
|
|
||||||
fn has_more(&self, cx: &App) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a threshold value (n entities), of course,
|
|
||||||
/// when scrolling to the bottom, the remaining number of rows
|
|
||||||
/// triggers `load_more`.
|
|
||||||
///
|
|
||||||
/// This should smaller than the total number of first load rows.
|
|
||||||
///
|
|
||||||
/// Default: 20 entities (section header, footer and row)
|
|
||||||
fn load_more_threshold(&self) -> usize {
|
|
||||||
20
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Load more data when the table is scrolled to the bottom.
|
|
||||||
///
|
|
||||||
/// This will performed in a background task.
|
|
||||||
///
|
|
||||||
/// This is always called when the table is near the bottom,
|
|
||||||
/// so you must check if there is more data to load or lock
|
|
||||||
/// the loading state.
|
|
||||||
fn load_more(&mut self, window: &mut Window, cx: &mut Context<ListState<Self>>) {}
|
|
||||||
}
|
|
||||||
@@ -1,747 +0,0 @@
|
|||||||
use std::ops::Range;
|
|
||||||
|
|
||||||
use gpui::prelude::FluentBuilder;
|
|
||||||
use gpui::{
|
|
||||||
App, AppContext, AvailableSpace, ClickEvent, Context, DefiniteLength, EdgesRefinement, Entity,
|
|
||||||
EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, KeyBinding, Length,
|
|
||||||
ListSizingBehavior, MouseButton, ParentElement, Render, RenderOnce, ScrollStrategy,
|
|
||||||
SharedString, StatefulInteractiveElement, StyleRefinement, Styled, Subscription, Task,
|
|
||||||
UniformListScrollHandle, Window, div, px, size, uniform_list,
|
|
||||||
};
|
|
||||||
use instant::Duration;
|
|
||||||
use theme::ActiveTheme;
|
|
||||||
|
|
||||||
use crate::actions::{Cancel, Confirm, SelectDown, SelectUp};
|
|
||||||
use crate::input::{Input, InputEvent, InputState};
|
|
||||||
use crate::list::ListDelegate;
|
|
||||||
use crate::list::cache::{MeasuredEntrySize, RowEntry, RowsCache};
|
|
||||||
use crate::scroll::{Scrollbar, ScrollbarHandle};
|
|
||||||
use crate::{Icon, IconName, IndexPath, Selectable, Sizable, Size, StyledExt, v_flex};
|
|
||||||
|
|
||||||
pub(crate) fn init(cx: &mut App) {
|
|
||||||
let context: Option<&str> = Some("List");
|
|
||||||
cx.bind_keys([
|
|
||||||
KeyBinding::new("escape", Cancel, context),
|
|
||||||
KeyBinding::new("enter", Confirm { secondary: false }, context),
|
|
||||||
KeyBinding::new("secondary-enter", Confirm { secondary: true }, context),
|
|
||||||
KeyBinding::new("up", SelectUp, context),
|
|
||||||
KeyBinding::new("down", SelectDown, context),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub enum ListEvent {
|
|
||||||
/// Move to select item.
|
|
||||||
Select(IndexPath),
|
|
||||||
/// Click on item or pressed Enter.
|
|
||||||
Confirm(IndexPath),
|
|
||||||
/// Pressed ESC to deselect the item.
|
|
||||||
Cancel,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ListOptions {
|
|
||||||
size: Size,
|
|
||||||
scrollbar_visible: bool,
|
|
||||||
search_placeholder: Option<SharedString>,
|
|
||||||
max_height: Option<Length>,
|
|
||||||
paddings: EdgesRefinement<DefiniteLength>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for ListOptions {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
size: Size::default(),
|
|
||||||
scrollbar_visible: true,
|
|
||||||
max_height: None,
|
|
||||||
search_placeholder: None,
|
|
||||||
paddings: EdgesRefinement::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The state for List.
|
|
||||||
///
|
|
||||||
/// List required all items has the same height.
|
|
||||||
pub struct ListState<D: ListDelegate> {
|
|
||||||
pub(crate) focus_handle: FocusHandle,
|
|
||||||
pub(crate) query_input: Entity<InputState>,
|
|
||||||
options: ListOptions,
|
|
||||||
delegate: D,
|
|
||||||
last_query: Option<String>,
|
|
||||||
scroll_handle: UniformListScrollHandle,
|
|
||||||
rows_cache: RowsCache,
|
|
||||||
selected_index: Option<IndexPath>,
|
|
||||||
item_to_measure_index: IndexPath,
|
|
||||||
deferred_scroll_to_index: Option<(IndexPath, ScrollStrategy)>,
|
|
||||||
mouse_right_clicked_index: Option<IndexPath>,
|
|
||||||
reset_on_cancel: bool,
|
|
||||||
searchable: bool,
|
|
||||||
selectable: bool,
|
|
||||||
_search_task: Task<()>,
|
|
||||||
_load_more_task: Task<()>,
|
|
||||||
_query_input_subscription: Subscription,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<D> ListState<D>
|
|
||||||
where
|
|
||||||
D: ListDelegate,
|
|
||||||
{
|
|
||||||
pub fn new(delegate: D, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
|
||||||
let query_input = cx.new(|cx| InputState::new(window, cx).placeholder("Search..."));
|
|
||||||
let _query_input_subscription =
|
|
||||||
cx.subscribe_in(&query_input, window, Self::on_query_input_event);
|
|
||||||
|
|
||||||
Self {
|
|
||||||
focus_handle: cx.focus_handle(),
|
|
||||||
options: ListOptions::default(),
|
|
||||||
delegate,
|
|
||||||
rows_cache: RowsCache::default(),
|
|
||||||
query_input,
|
|
||||||
last_query: None,
|
|
||||||
selected_index: None,
|
|
||||||
selectable: true,
|
|
||||||
searchable: false,
|
|
||||||
item_to_measure_index: IndexPath::default(),
|
|
||||||
deferred_scroll_to_index: None,
|
|
||||||
mouse_right_clicked_index: None,
|
|
||||||
scroll_handle: UniformListScrollHandle::new(),
|
|
||||||
reset_on_cancel: true,
|
|
||||||
_search_task: Task::ready(()),
|
|
||||||
_load_more_task: Task::ready(()),
|
|
||||||
_query_input_subscription,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets whether the list is searchable, default is `false`.
|
|
||||||
///
|
|
||||||
/// When `true`, there will be a search input at the top of the list.
|
|
||||||
pub fn searchable(mut self, searchable: bool) -> Self {
|
|
||||||
self.searchable = searchable;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_searchable(&mut self, searchable: bool, cx: &mut Context<Self>) {
|
|
||||||
self.searchable = searchable;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets whether the list is selectable, default is true.
|
|
||||||
pub fn selectable(mut self, selectable: bool) -> Self {
|
|
||||||
self.selectable = selectable;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets whether the list is selectable, default is true.
|
|
||||||
pub fn set_selectable(&mut self, selectable: bool, cx: &mut Context<Self>) {
|
|
||||||
self.selectable = selectable;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn delegate(&self) -> &D {
|
|
||||||
&self.delegate
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn delegate_mut(&mut self) -> &mut D {
|
|
||||||
&mut self.delegate
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Focus the list, if the list is searchable, focus the search input.
|
|
||||||
pub fn focus(&mut self, window: &mut Window, cx: &mut App) {
|
|
||||||
self.focus_handle(cx).focus(window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return true if either the list or the search input is focused.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub(crate) fn is_focused(&self, window: &Window, cx: &App) -> bool {
|
|
||||||
self.focus_handle.is_focused(window) || self.query_input.focus_handle(cx).is_focused(window)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the selected index of the list,
|
|
||||||
/// this will also scroll to the selected item.
|
|
||||||
pub(crate) fn _set_selected_index(
|
|
||||||
&mut self,
|
|
||||||
ix: Option<IndexPath>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
if !self.selectable {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.selected_index = ix;
|
|
||||||
self.delegate.set_selected_index(ix, window, cx);
|
|
||||||
self.scroll_to_selected_item(window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the selected index of the list,
|
|
||||||
/// this method will not scroll to the selected item.
|
|
||||||
pub fn set_selected_index(
|
|
||||||
&mut self,
|
|
||||||
ix: Option<IndexPath>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
self.selected_index = ix;
|
|
||||||
self.delegate.set_selected_index(ix, window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn selected_index(&self) -> Option<IndexPath> {
|
|
||||||
self.selected_index
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the index of the item that has been right clicked.
|
|
||||||
pub fn set_right_clicked_index(
|
|
||||||
&mut self,
|
|
||||||
ix: Option<IndexPath>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
self.mouse_right_clicked_index = ix;
|
|
||||||
self.delegate.set_right_clicked_index(ix, window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the index of the item that has been right clicked.
|
|
||||||
pub fn right_clicked_index(&self) -> Option<IndexPath> {
|
|
||||||
self.mouse_right_clicked_index
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set a specific list item for measurement.
|
|
||||||
pub fn set_item_to_measure_index(
|
|
||||||
&mut self,
|
|
||||||
ix: IndexPath,
|
|
||||||
_: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
self.item_to_measure_index = ix;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Scroll to the item at the given index.
|
|
||||||
pub fn scroll_to_item(
|
|
||||||
&mut self,
|
|
||||||
ix: IndexPath,
|
|
||||||
strategy: ScrollStrategy,
|
|
||||||
_: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
if ix.section == 0 && ix.row == 0 {
|
|
||||||
// If the item is the first item, scroll to the top.
|
|
||||||
let mut offset = self.scroll_handle.offset();
|
|
||||||
offset.y = px(0.);
|
|
||||||
self.scroll_handle.set_offset(offset);
|
|
||||||
cx.notify();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
self.deferred_scroll_to_index = Some((ix, strategy));
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get scroll handle
|
|
||||||
pub fn scroll_handle(&self) -> &UniformListScrollHandle {
|
|
||||||
&self.scroll_handle
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn scroll_to_selected_item(&mut self, _: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
if let Some(ix) = self.selected_index {
|
|
||||||
self.deferred_scroll_to_index = Some((ix, ScrollStrategy::Top));
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_query_input_event(
|
|
||||||
&mut self,
|
|
||||||
state: &Entity<InputState>,
|
|
||||||
event: &InputEvent,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
match event {
|
|
||||||
InputEvent::Change => {
|
|
||||||
let text = state.read(cx).value();
|
|
||||||
let text = text.trim().to_string();
|
|
||||||
if Some(&text) == self.last_query.as_ref() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.set_searching(true, window, cx);
|
|
||||||
|
|
||||||
let search = self.delegate.perform_search(&text, window, cx);
|
|
||||||
|
|
||||||
if self.rows_cache.len() > 0 {
|
|
||||||
self._set_selected_index(Some(IndexPath::default()), window, cx);
|
|
||||||
} else {
|
|
||||||
self._set_selected_index(None, window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
let executor = cx.background_executor().clone();
|
|
||||||
self._search_task = cx.spawn_in(window, async move |this, window| {
|
|
||||||
search.await;
|
|
||||||
|
|
||||||
_ = this.update_in(window, |this, _, _| {
|
|
||||||
this.scroll_handle.scroll_to_item(0, ScrollStrategy::Top);
|
|
||||||
this.last_query = Some(text);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Always wait 100ms to avoid flicker
|
|
||||||
executor.timer(Duration::from_millis(100)).await;
|
|
||||||
|
|
||||||
_ = this.update_in(window, |this, window, cx| {
|
|
||||||
this.set_searching(false, window, cx);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
InputEvent::PressEnter { secondary, .. } => self.on_action_confirm(
|
|
||||||
&Confirm {
|
|
||||||
secondary: *secondary,
|
|
||||||
},
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
),
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_searching(&mut self, searching: bool, _window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
self.query_input
|
|
||||||
.update(cx, |input, cx| input.set_loading(searching, cx));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Dispatch delegate's `load_more` method when the
|
|
||||||
/// visible range is near the end.
|
|
||||||
fn load_more_if_need(
|
|
||||||
&mut self,
|
|
||||||
entities_count: usize,
|
|
||||||
visible_end: usize,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
// FIXME: Here need void sections items count.
|
|
||||||
|
|
||||||
let threshold = self.delegate.load_more_threshold();
|
|
||||||
// Securely handle subtract logic to prevent attempt
|
|
||||||
// to subtract with overflow
|
|
||||||
if visible_end >= entities_count.saturating_sub(threshold) {
|
|
||||||
if !self.delegate.has_more(cx) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self._load_more_task = cx.spawn_in(window, async move |view, cx| {
|
|
||||||
_ = view.update_in(cx, |view, window, cx| {
|
|
||||||
view.delegate.load_more(window, cx);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub(crate) fn reset_on_cancel(mut self, reset: bool) -> Self {
|
|
||||||
self.reset_on_cancel = reset;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_action_cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
cx.propagate();
|
|
||||||
if self.reset_on_cancel {
|
|
||||||
self._set_selected_index(None, window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.delegate.cancel(window, cx);
|
|
||||||
cx.emit(ListEvent::Cancel);
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_action_confirm(
|
|
||||||
&mut self,
|
|
||||||
confirm: &Confirm,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
if self.rows_cache.len() == 0 {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let Some(ix) = self.selected_index else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
self.delegate
|
|
||||||
.set_selected_index(self.selected_index, window, cx);
|
|
||||||
self.delegate.confirm(confirm.secondary, window, cx);
|
|
||||||
cx.emit(ListEvent::Confirm(ix));
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn select_item(&mut self, ix: IndexPath, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
if !self.selectable {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.selected_index = Some(ix);
|
|
||||||
self.delegate.set_selected_index(Some(ix), window, cx);
|
|
||||||
self.scroll_to_selected_item(window, cx);
|
|
||||||
cx.emit(ListEvent::Select(ix));
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn on_action_select_prev(
|
|
||||||
&mut self,
|
|
||||||
_: &SelectUp,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
if self.rows_cache.len() == 0 {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let prev_ix = self.rows_cache.prev(self.selected_index);
|
|
||||||
self.select_item(prev_ix, window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn on_action_select_next(
|
|
||||||
&mut self,
|
|
||||||
_: &SelectDown,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
if self.rows_cache.len() == 0 {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let next_ix = self.rows_cache.next(self.selected_index);
|
|
||||||
self.select_item(next_ix, window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prepare_items_if_needed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
let sections_count = self.delegate.sections_count(cx).max(1);
|
|
||||||
let mut measured_size = MeasuredEntrySize::default();
|
|
||||||
|
|
||||||
// Measure the item_height and section header/footer height.
|
|
||||||
let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent);
|
|
||||||
measured_size.item_size = self
|
|
||||||
.render_list_item(self.item_to_measure_index, window, cx)
|
|
||||||
.into_any_element()
|
|
||||||
.layout_as_root(available_space, window, cx);
|
|
||||||
|
|
||||||
if let Some(mut el) = self
|
|
||||||
.delegate
|
|
||||||
.render_section_header(0, window, cx)
|
|
||||||
.map(|r| r.into_any_element())
|
|
||||||
{
|
|
||||||
measured_size.section_header_size = el.layout_as_root(available_space, window, cx);
|
|
||||||
}
|
|
||||||
if let Some(mut el) = self
|
|
||||||
.delegate
|
|
||||||
.render_section_footer(0, window, cx)
|
|
||||||
.map(|r| r.into_any_element())
|
|
||||||
{
|
|
||||||
measured_size.section_footer_size = el.layout_as_root(available_space, window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.rows_cache
|
|
||||||
.prepare_if_needed(sections_count, measured_size, cx, |section_ix, cx| {
|
|
||||||
self.delegate.items_count(section_ix, cx)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_list_item(
|
|
||||||
&mut self,
|
|
||||||
ix: IndexPath,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) -> impl IntoElement {
|
|
||||||
let selectable = self.selectable;
|
|
||||||
let selected = self.selected_index.map(|s| s.eq_row(ix)).unwrap_or(false);
|
|
||||||
let mouse_right_clicked = self
|
|
||||||
.mouse_right_clicked_index
|
|
||||||
.map(|s| s.eq_row(ix))
|
|
||||||
.unwrap_or(false);
|
|
||||||
let id = SharedString::from(format!("list-item-{}", ix));
|
|
||||||
|
|
||||||
div()
|
|
||||||
.id(id)
|
|
||||||
.w_full()
|
|
||||||
.relative()
|
|
||||||
.overflow_hidden()
|
|
||||||
.children(self.delegate.render_item(ix, window, cx).map(|item| {
|
|
||||||
item.selected(selected)
|
|
||||||
.secondary_selected(mouse_right_clicked)
|
|
||||||
}))
|
|
||||||
.when(selectable, |this| {
|
|
||||||
this.on_click(cx.listener(move |this, e: &ClickEvent, window, cx| {
|
|
||||||
this.set_right_clicked_index(None, window, cx);
|
|
||||||
this.selected_index = Some(ix);
|
|
||||||
this.on_action_confirm(
|
|
||||||
&Confirm {
|
|
||||||
secondary: e.modifiers().secondary(),
|
|
||||||
},
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
}))
|
|
||||||
.on_mouse_down(
|
|
||||||
MouseButton::Right,
|
|
||||||
cx.listener(move |this, _, window, cx| {
|
|
||||||
this.set_right_clicked_index(Some(ix), window, cx);
|
|
||||||
cx.notify();
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_items(
|
|
||||||
&mut self,
|
|
||||||
items_count: usize,
|
|
||||||
entities_count: usize,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) -> impl IntoElement {
|
|
||||||
let rows_cache = self.rows_cache.clone();
|
|
||||||
let scrollbar_visible = self.options.scrollbar_visible;
|
|
||||||
let scroll_handle = self.scroll_handle.clone();
|
|
||||||
|
|
||||||
v_flex()
|
|
||||||
.flex_grow_1()
|
|
||||||
.relative()
|
|
||||||
.size_full()
|
|
||||||
.when_some(self.options.max_height, |this, h| this.max_h(h))
|
|
||||||
.overflow_hidden()
|
|
||||||
.when(items_count == 0, |this| {
|
|
||||||
this.child(self.delegate.render_empty(window, cx))
|
|
||||||
})
|
|
||||||
.when(items_count > 0, {
|
|
||||||
|this| {
|
|
||||||
this.child(
|
|
||||||
uniform_list(
|
|
||||||
"virtual-list",
|
|
||||||
rows_cache.items_count(),
|
|
||||||
cx.processor(move |this, range: Range<usize>, window, cx| {
|
|
||||||
this.load_more_if_need(entities_count, range.end, window, cx);
|
|
||||||
|
|
||||||
// NOTE: Here the v_virtual_list would not able to have gap_y,
|
|
||||||
// because the section header, footer is always have rendered as a empty child item,
|
|
||||||
// even the delegate give a None result.
|
|
||||||
|
|
||||||
range
|
|
||||||
.map(|ix| {
|
|
||||||
let Some(entry) = rows_cache.get(ix) else {
|
|
||||||
return div();
|
|
||||||
};
|
|
||||||
|
|
||||||
div().children(match entry {
|
|
||||||
RowEntry::Entry(index) => Some(
|
|
||||||
this.render_list_item(index, window, cx)
|
|
||||||
.into_any_element(),
|
|
||||||
),
|
|
||||||
RowEntry::SectionHeader(section_ix) => this
|
|
||||||
.delegate_mut()
|
|
||||||
.render_section_header(section_ix, window, cx)
|
|
||||||
.map(|r| r.into_any_element()),
|
|
||||||
RowEntry::SectionFooter(section_ix) => this
|
|
||||||
.delegate_mut()
|
|
||||||
.render_section_footer(section_ix, window, cx)
|
|
||||||
.map(|r| r.into_any_element()),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.when(self.options.max_height.is_some(), |this| {
|
|
||||||
this.with_sizing_behavior(ListSizingBehavior::Infer)
|
|
||||||
})
|
|
||||||
.track_scroll(&scroll_handle)
|
|
||||||
.into_any_element(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.when(scrollbar_visible, |this| {
|
|
||||||
this.child(Scrollbar::vertical(&scroll_handle))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<D> Focusable for ListState<D>
|
|
||||||
where
|
|
||||||
D: ListDelegate,
|
|
||||||
{
|
|
||||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
|
||||||
if self.searchable {
|
|
||||||
self.query_input.focus_handle(cx)
|
|
||||||
} else {
|
|
||||||
self.focus_handle.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<D> EventEmitter<ListEvent> for ListState<D> where D: ListDelegate {}
|
|
||||||
impl<D> Render for ListState<D>
|
|
||||||
where
|
|
||||||
D: ListDelegate,
|
|
||||||
{
|
|
||||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
self.prepare_items_if_needed(window, cx);
|
|
||||||
|
|
||||||
// Scroll to the selected item if it is set.
|
|
||||||
if let Some((ix, strategy)) = self.deferred_scroll_to_index.take()
|
|
||||||
&& let Some(item_ix) = self.rows_cache.position_of(&ix)
|
|
||||||
{
|
|
||||||
self.scroll_handle.scroll_to_item(item_ix, strategy);
|
|
||||||
}
|
|
||||||
|
|
||||||
let loading = self.delegate().loading(cx);
|
|
||||||
let query_input = if self.searchable {
|
|
||||||
// sync placeholder
|
|
||||||
if let Some(placeholder) = &self.options.search_placeholder {
|
|
||||||
self.query_input.update(cx, |input, cx| {
|
|
||||||
input.set_placeholder(placeholder.clone(), window, cx);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Some(self.query_input.clone())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let loading_view = if loading {
|
|
||||||
Some(self.delegate.render_loading(window, cx).into_any_element())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
let initial_view = if let Some(input) = &query_input {
|
|
||||||
if input.read(cx).value().is_empty() {
|
|
||||||
self.delegate.render_initial(window, cx)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
let items_count = self.rows_cache.items_count();
|
|
||||||
let entities_count = self.rows_cache.len();
|
|
||||||
let mouse_right_clicked_index = self.mouse_right_clicked_index;
|
|
||||||
|
|
||||||
v_flex()
|
|
||||||
.key_context("List")
|
|
||||||
.id("list-state")
|
|
||||||
.track_focus(&self.focus_handle)
|
|
||||||
.size_full()
|
|
||||||
.relative()
|
|
||||||
.overflow_hidden()
|
|
||||||
.when_some(query_input, |this, input| {
|
|
||||||
this.child(
|
|
||||||
div()
|
|
||||||
.map(|this| match self.options.size {
|
|
||||||
Size::Small => this.px_1p5(),
|
|
||||||
_ => this.px_2(),
|
|
||||||
})
|
|
||||||
.border_b_1()
|
|
||||||
.border_color(cx.theme().border)
|
|
||||||
.child(
|
|
||||||
Input::new(&input)
|
|
||||||
.with_size(self.options.size)
|
|
||||||
.appearance(false)
|
|
||||||
.cleanable(true)
|
|
||||||
.p_0()
|
|
||||||
.prefix(
|
|
||||||
Icon::new(IconName::Search).text_color(cx.theme().text_muted),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.when(!loading, |this| {
|
|
||||||
this.on_action(cx.listener(Self::on_action_cancel))
|
|
||||||
.on_action(cx.listener(Self::on_action_confirm))
|
|
||||||
.on_action(cx.listener(Self::on_action_select_next))
|
|
||||||
.on_action(cx.listener(Self::on_action_select_prev))
|
|
||||||
.map(|this| {
|
|
||||||
if let Some(view) = initial_view {
|
|
||||||
this.child(view)
|
|
||||||
} else {
|
|
||||||
this.child(self.render_items(items_count, entities_count, window, cx))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// Click out to cancel right clicked row
|
|
||||||
.when(mouse_right_clicked_index.is_some(), |this| {
|
|
||||||
this.on_mouse_down_out(cx.listener(|this, _, window, cx| {
|
|
||||||
this.set_right_clicked_index(None, window, cx);
|
|
||||||
cx.notify();
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.children(loading_view)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The List element.
|
|
||||||
#[derive(IntoElement)]
|
|
||||||
pub struct List<D: ListDelegate + 'static> {
|
|
||||||
state: Entity<ListState<D>>,
|
|
||||||
style: StyleRefinement,
|
|
||||||
options: ListOptions,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<D> List<D>
|
|
||||||
where
|
|
||||||
D: ListDelegate + 'static,
|
|
||||||
{
|
|
||||||
/// Create a new List element with the given ListState entity.
|
|
||||||
pub fn new(state: &Entity<ListState<D>>) -> Self {
|
|
||||||
Self {
|
|
||||||
state: state.clone(),
|
|
||||||
style: StyleRefinement::default(),
|
|
||||||
options: ListOptions::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set whether the scrollbar is visible, default is `true`.
|
|
||||||
pub fn scrollbar_visible(mut self, visible: bool) -> Self {
|
|
||||||
self.options.scrollbar_visible = visible;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the placeholder text for the search input.
|
|
||||||
pub fn search_placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
|
|
||||||
self.options.search_placeholder = Some(placeholder.into());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<D> Styled for List<D>
|
|
||||||
where
|
|
||||||
D: ListDelegate + 'static,
|
|
||||||
{
|
|
||||||
fn style(&mut self) -> &mut StyleRefinement {
|
|
||||||
&mut self.style
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<D> Sizable for List<D>
|
|
||||||
where
|
|
||||||
D: ListDelegate + 'static,
|
|
||||||
{
|
|
||||||
fn with_size(mut self, size: impl Into<Size>) -> Self {
|
|
||||||
self.options.size = size.into();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<D> RenderOnce for List<D>
|
|
||||||
where
|
|
||||||
D: ListDelegate + 'static,
|
|
||||||
{
|
|
||||||
fn render(mut self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
|
||||||
// Take paddings, max_height to options, and clear them from style,
|
|
||||||
// because they would be applied to the inner virtual list.
|
|
||||||
self.options.paddings = self.style.padding.clone();
|
|
||||||
self.options.max_height = self.style.max_size.height;
|
|
||||||
self.style.padding = EdgesRefinement::default();
|
|
||||||
self.style.max_size.height = None;
|
|
||||||
|
|
||||||
self.state.update(cx, |state, _| {
|
|
||||||
state.options = self.options;
|
|
||||||
});
|
|
||||||
|
|
||||||
div()
|
|
||||||
.id("list")
|
|
||||||
.size_full()
|
|
||||||
.refine_style(&self.style)
|
|
||||||
.child(self.state.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,226 +0,0 @@
|
|||||||
use gpui::prelude::FluentBuilder as _;
|
|
||||||
use gpui::{
|
|
||||||
div, AnyElement, App, ClickEvent, Div, ElementId, InteractiveElement, IntoElement,
|
|
||||||
MouseMoveEvent, ParentElement, RenderOnce, Stateful, StatefulInteractiveElement as _,
|
|
||||||
StyleRefinement, Styled, Window,
|
|
||||||
};
|
|
||||||
use smallvec::SmallVec;
|
|
||||||
use theme::ActiveTheme;
|
|
||||||
|
|
||||||
use crate::{h_flex, Disableable, Icon, Selectable, Sizable as _, StyledExt};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
|
||||||
enum ListItemMode {
|
|
||||||
#[default]
|
|
||||||
Entry,
|
|
||||||
Separator,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ListItemMode {
|
|
||||||
#[inline]
|
|
||||||
fn is_separator(&self) -> bool {
|
|
||||||
matches!(self, ListItemMode::Separator)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
|
||||||
pub struct ListItem {
|
|
||||||
base: Stateful<Div>,
|
|
||||||
mode: ListItemMode,
|
|
||||||
style: StyleRefinement,
|
|
||||||
disabled: bool,
|
|
||||||
selected: bool,
|
|
||||||
secondary_selected: bool,
|
|
||||||
confirmed: bool,
|
|
||||||
check_icon: Option<Icon>,
|
|
||||||
#[allow(clippy::type_complexity)]
|
|
||||||
on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
|
|
||||||
#[allow(clippy::type_complexity)]
|
|
||||||
on_mouse_enter: Option<Box<dyn Fn(&MouseMoveEvent, &mut Window, &mut App) + 'static>>,
|
|
||||||
#[allow(clippy::type_complexity)]
|
|
||||||
suffix: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyElement + 'static>>,
|
|
||||||
children: SmallVec<[AnyElement; 2]>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ListItem {
|
|
||||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
|
||||||
let id: ElementId = id.into();
|
|
||||||
Self {
|
|
||||||
mode: ListItemMode::Entry,
|
|
||||||
base: h_flex().id(id),
|
|
||||||
style: StyleRefinement::default(),
|
|
||||||
disabled: false,
|
|
||||||
selected: false,
|
|
||||||
secondary_selected: false,
|
|
||||||
confirmed: false,
|
|
||||||
on_click: None,
|
|
||||||
on_mouse_enter: None,
|
|
||||||
check_icon: None,
|
|
||||||
suffix: None,
|
|
||||||
children: SmallVec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set this list item to as a separator, it not able to be selected.
|
|
||||||
pub fn separator(mut self) -> Self {
|
|
||||||
self.mode = ListItemMode::Separator;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set to show check icon, default is None.
|
|
||||||
pub fn check_icon(mut self, icon: impl Into<Icon>) -> Self {
|
|
||||||
self.check_icon = Some(icon.into());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set ListItem as the selected item style.
|
|
||||||
pub fn selected(mut self, selected: bool) -> Self {
|
|
||||||
self.selected = selected;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set ListItem as the confirmed item style, it will show a check icon.
|
|
||||||
pub fn confirmed(mut self, confirmed: bool) -> Self {
|
|
||||||
self.confirmed = confirmed;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn disabled(mut self, disabled: bool) -> Self {
|
|
||||||
self.disabled = disabled;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the suffix element of the input field, for example a clear button.
|
|
||||||
pub fn suffix<F, E>(mut self, builder: F) -> Self
|
|
||||||
where
|
|
||||||
F: Fn(&mut Window, &mut App) -> E + 'static,
|
|
||||||
E: IntoElement,
|
|
||||||
{
|
|
||||||
self.suffix = Some(Box::new(move |window, cx| {
|
|
||||||
builder(window, cx).into_any_element()
|
|
||||||
}));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn on_click(
|
|
||||||
mut self,
|
|
||||||
handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
|
||||||
) -> Self {
|
|
||||||
self.on_click = Some(Box::new(handler));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn on_mouse_enter(
|
|
||||||
mut self,
|
|
||||||
handler: impl Fn(&MouseMoveEvent, &mut Window, &mut App) + 'static,
|
|
||||||
) -> Self {
|
|
||||||
self.on_mouse_enter = Some(Box::new(handler));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Disableable for ListItem {
|
|
||||||
fn disabled(mut self, disabled: bool) -> Self {
|
|
||||||
self.disabled = disabled;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Selectable for ListItem {
|
|
||||||
fn selected(mut self, selected: bool) -> Self {
|
|
||||||
self.selected = selected;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_selected(&self) -> bool {
|
|
||||||
self.selected
|
|
||||||
}
|
|
||||||
|
|
||||||
fn secondary_selected(mut self, selected: bool) -> Self {
|
|
||||||
self.secondary_selected = selected;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Styled for ListItem {
|
|
||||||
fn style(&mut self) -> &mut gpui::StyleRefinement {
|
|
||||||
&mut self.style
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ParentElement for ListItem {
|
|
||||||
fn extend(&mut self, elements: impl IntoIterator<Item = gpui::AnyElement>) {
|
|
||||||
self.children.extend(elements);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenderOnce for ListItem {
|
|
||||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
|
||||||
let is_active = self.confirmed || self.selected;
|
|
||||||
|
|
||||||
let corner_radii = self.style.corner_radii.clone();
|
|
||||||
|
|
||||||
let _selected_style = StyleRefinement {
|
|
||||||
corner_radii,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let is_selectable = !(self.disabled || self.mode.is_separator());
|
|
||||||
|
|
||||||
self.base
|
|
||||||
.relative()
|
|
||||||
.gap_x_1()
|
|
||||||
.py_1()
|
|
||||||
.px_3()
|
|
||||||
.text_base()
|
|
||||||
.text_color(cx.theme().text)
|
|
||||||
.relative()
|
|
||||||
.items_center()
|
|
||||||
.justify_between()
|
|
||||||
.refine_style(&self.style)
|
|
||||||
.when(is_selectable, |this| {
|
|
||||||
this.when_some(self.on_click, |this, on_click| this.on_click(on_click))
|
|
||||||
.when_some(self.on_mouse_enter, |this, on_mouse_enter| {
|
|
||||||
this.on_mouse_move(move |ev, window, cx| (on_mouse_enter)(ev, window, cx))
|
|
||||||
})
|
|
||||||
.when(!is_active, |this| {
|
|
||||||
this.hover(|this| this.bg(cx.theme().ghost_element_hover))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.when(!is_selectable, |this| {
|
|
||||||
this.text_color(cx.theme().text_muted)
|
|
||||||
})
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.w_full()
|
|
||||||
.items_center()
|
|
||||||
.justify_between()
|
|
||||||
.gap_x_1()
|
|
||||||
.child(div().w_full().children(self.children))
|
|
||||||
.when_some(self.check_icon, |this, icon| {
|
|
||||||
this.child(
|
|
||||||
div()
|
|
||||||
.w_5()
|
|
||||||
.items_center()
|
|
||||||
.justify_center()
|
|
||||||
.when(self.confirmed, |this| {
|
|
||||||
this.child(icon.small().text_color(cx.theme().text_muted))
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.when_some(self.suffix, |this, suffix| this.child(suffix(window, cx)))
|
|
||||||
.map(|this| {
|
|
||||||
if is_selectable && (self.selected || self.secondary_selected) {
|
|
||||||
let bg = if self.selected {
|
|
||||||
cx.theme().ghost_element_active
|
|
||||||
} else {
|
|
||||||
cx.theme().ghost_element_background
|
|
||||||
};
|
|
||||||
this.bg(bg)
|
|
||||||
} else {
|
|
||||||
this
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
use gpui::{IntoElement, ParentElement as _, RenderOnce, Styled};
|
|
||||||
|
|
||||||
use super::ListItem;
|
|
||||||
use crate::skeleton::Skeleton;
|
|
||||||
use crate::v_flex;
|
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
|
||||||
pub struct Loading;
|
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
|
||||||
struct LoadingItem;
|
|
||||||
|
|
||||||
impl RenderOnce for LoadingItem {
|
|
||||||
fn render(self, _window: &mut gpui::Window, _cx: &mut gpui::App) -> impl IntoElement {
|
|
||||||
ListItem::new("skeleton").disabled(true).child(
|
|
||||||
v_flex()
|
|
||||||
.gap_1p5()
|
|
||||||
.overflow_hidden()
|
|
||||||
.child(Skeleton::new().h_5().w_48().max_w_full())
|
|
||||||
.child(Skeleton::new().secondary().h_3().w_64().max_w_full()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenderOnce for Loading {
|
|
||||||
fn render(self, _window: &mut gpui::Window, _cx: &mut gpui::App) -> impl IntoElement {
|
|
||||||
v_flex()
|
|
||||||
.py_2p5()
|
|
||||||
.gap_3()
|
|
||||||
.child(LoadingItem)
|
|
||||||
.child(LoadingItem)
|
|
||||||
.child(LoadingItem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
pub(crate) mod cache;
|
|
||||||
mod delegate;
|
|
||||||
#[allow(clippy::module_inception)]
|
|
||||||
mod list;
|
|
||||||
mod list_item;
|
|
||||||
mod loading;
|
|
||||||
mod separator_item;
|
|
||||||
|
|
||||||
pub use delegate::*;
|
|
||||||
pub use list::*;
|
|
||||||
pub use list_item::*;
|
|
||||||
pub use separator_item::*;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
/// Settings for List.
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct ListSettings {
|
|
||||||
/// Whether to use active highlight style on ListItem, default
|
|
||||||
pub active_highlight: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for ListSettings {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
active_highlight: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
use gpui::{AnyElement, ParentElement, RenderOnce, StyleRefinement};
|
|
||||||
use smallvec::SmallVec;
|
|
||||||
|
|
||||||
use crate::list::ListItem;
|
|
||||||
use crate::{Selectable, StyledExt};
|
|
||||||
|
|
||||||
pub struct ListSeparatorItem {
|
|
||||||
style: StyleRefinement,
|
|
||||||
children: SmallVec<[AnyElement; 2]>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ListSeparatorItem {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
style: StyleRefinement::default(),
|
|
||||||
children: SmallVec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for ListSeparatorItem {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ParentElement for ListSeparatorItem {
|
|
||||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
|
||||||
self.children.extend(elements);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Selectable for ListSeparatorItem {
|
|
||||||
fn selected(self, _: bool) -> Self {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_selected(&self) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenderOnce for ListSeparatorItem {
|
|
||||||
fn render(self, _: &mut gpui::Window, _: &mut gpui::App) -> impl gpui::IntoElement {
|
|
||||||
ListItem::new("separator")
|
|
||||||
.refine_style(&self.style)
|
|
||||||
.children(self.children)
|
|
||||||
.disabled(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,257 +0,0 @@
|
|||||||
use gpui::prelude::FluentBuilder;
|
|
||||||
use gpui::{
|
|
||||||
App, AppContext as _, ClickEvent, Context, DismissEvent, Entity, Focusable,
|
|
||||||
InteractiveElement as _, IntoElement, KeyBinding, MouseButton, OwnedMenu, ParentElement,
|
|
||||||
Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Window, anchored,
|
|
||||||
deferred, div, px,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::actions::{Cancel, SelectLeft, SelectRight};
|
|
||||||
use crate::button::{Button, ButtonVariants};
|
|
||||||
use crate::menu::PopupMenu;
|
|
||||||
use crate::{Selectable, Sizable, h_flex};
|
|
||||||
|
|
||||||
const CONTEXT: &str = "AppMenuBar";
|
|
||||||
|
|
||||||
pub fn init(cx: &mut App) {
|
|
||||||
cx.bind_keys([
|
|
||||||
KeyBinding::new("escape", Cancel, Some(CONTEXT)),
|
|
||||||
KeyBinding::new("left", SelectLeft, Some(CONTEXT)),
|
|
||||||
KeyBinding::new("right", SelectRight, Some(CONTEXT)),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The application menu bar, for Windows and Linux.
|
|
||||||
pub struct AppMenuBar {
|
|
||||||
menus: Vec<Entity<AppMenu>>,
|
|
||||||
selected_index: Option<usize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AppMenuBar {
|
|
||||||
/// Create a new app menu bar.
|
|
||||||
pub fn new(cx: &mut App) -> Entity<Self> {
|
|
||||||
cx.new(|cx| {
|
|
||||||
let mut this = Self {
|
|
||||||
selected_index: None,
|
|
||||||
menus: Vec::new(),
|
|
||||||
};
|
|
||||||
this.reload(cx);
|
|
||||||
this
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reload the menus from the app.
|
|
||||||
pub fn reload(&mut self, cx: &mut Context<Self>) {
|
|
||||||
let menu_bar = cx.entity();
|
|
||||||
self.menus = cx
|
|
||||||
.get_menus()
|
|
||||||
.unwrap_or_default()
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(ix, menu)| AppMenu::new(ix, menu, menu_bar.clone(), cx))
|
|
||||||
.collect();
|
|
||||||
self.selected_index = None;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_move_left(&mut self, _: &SelectLeft, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
let Some(selected_index) = self.selected_index else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let new_ix = if selected_index == 0 {
|
|
||||||
self.menus.len().saturating_sub(1)
|
|
||||||
} else {
|
|
||||||
selected_index.saturating_sub(1)
|
|
||||||
};
|
|
||||||
self.set_selected_index(Some(new_ix), window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_move_right(&mut self, _: &SelectRight, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
let Some(selected_index) = self.selected_index else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let new_ix = if selected_index + 1 >= self.menus.len() {
|
|
||||||
0
|
|
||||||
} else {
|
|
||||||
selected_index + 1
|
|
||||||
};
|
|
||||||
self.set_selected_index(Some(new_ix), window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
self.set_selected_index(None, window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_selected_index(&mut self, ix: Option<usize>, _: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
self.selected_index = ix;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn has_activated_menu(&self) -> bool {
|
|
||||||
self.selected_index.is_some()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for AppMenuBar {
|
|
||||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
h_flex()
|
|
||||||
.id("app-menu-bar")
|
|
||||||
.key_context(CONTEXT)
|
|
||||||
.on_action(cx.listener(Self::on_move_left))
|
|
||||||
.on_action(cx.listener(Self::on_move_right))
|
|
||||||
.on_action(cx.listener(Self::on_cancel))
|
|
||||||
.size_full()
|
|
||||||
.gap_x_1()
|
|
||||||
.overflow_x_scroll()
|
|
||||||
.children(self.menus.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A menu in the menu bar.
|
|
||||||
pub(super) struct AppMenu {
|
|
||||||
menu_bar: Entity<AppMenuBar>,
|
|
||||||
ix: usize,
|
|
||||||
name: SharedString,
|
|
||||||
menu: OwnedMenu,
|
|
||||||
popup_menu: Option<Entity<PopupMenu>>,
|
|
||||||
|
|
||||||
_subscription: Option<Subscription>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AppMenu {
|
|
||||||
pub(super) fn new(
|
|
||||||
ix: usize,
|
|
||||||
menu: &OwnedMenu,
|
|
||||||
menu_bar: Entity<AppMenuBar>,
|
|
||||||
cx: &mut App,
|
|
||||||
) -> Entity<Self> {
|
|
||||||
let name = menu.name.clone();
|
|
||||||
cx.new(|_| Self {
|
|
||||||
ix,
|
|
||||||
menu_bar,
|
|
||||||
name,
|
|
||||||
menu: menu.clone(),
|
|
||||||
popup_menu: None,
|
|
||||||
_subscription: None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_popup_menu(
|
|
||||||
&mut self,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) -> Entity<PopupMenu> {
|
|
||||||
let popup_menu = match self.popup_menu.as_ref() {
|
|
||||||
None => {
|
|
||||||
let items = self.menu.items.clone();
|
|
||||||
let popup_menu = PopupMenu::build(window, cx, |menu, window, cx| {
|
|
||||||
menu.when_some(window.focused(cx), |this, handle| {
|
|
||||||
this.action_context(handle)
|
|
||||||
})
|
|
||||||
.with_menu_items(items, window, cx)
|
|
||||||
});
|
|
||||||
popup_menu.read(cx).focus_handle(cx).focus(window, cx);
|
|
||||||
self._subscription =
|
|
||||||
Some(cx.subscribe_in(&popup_menu, window, Self::handle_dismiss));
|
|
||||||
self.popup_menu = Some(popup_menu.clone());
|
|
||||||
|
|
||||||
popup_menu
|
|
||||||
}
|
|
||||||
Some(menu) => menu.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let focus_handle = popup_menu.read(cx).focus_handle(cx);
|
|
||||||
if !focus_handle.contains_focused(window, cx) {
|
|
||||||
focus_handle.focus(window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
popup_menu
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_dismiss(
|
|
||||||
&mut self,
|
|
||||||
_: &Entity<PopupMenu>,
|
|
||||||
_: &DismissEvent,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
self._subscription.take();
|
|
||||||
self.popup_menu.take();
|
|
||||||
self.menu_bar.update(cx, |state, cx| {
|
|
||||||
state.on_cancel(&Cancel, window, cx);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_trigger_click(
|
|
||||||
&mut self,
|
|
||||||
_: &ClickEvent,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
let is_selected = self.menu_bar.read(cx).selected_index == Some(self.ix);
|
|
||||||
|
|
||||||
self.menu_bar.update(cx, |state, cx| {
|
|
||||||
let new_ix = if is_selected { None } else { Some(self.ix) };
|
|
||||||
state.set_selected_index(new_ix, window, cx);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_hover(&mut self, hovered: &bool, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
if !*hovered {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let has_activated_menu = self.menu_bar.read(cx).has_activated_menu();
|
|
||||||
if !has_activated_menu {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.menu_bar.update(cx, |state, cx| {
|
|
||||||
state.set_selected_index(Some(self.ix), window, cx);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for AppMenu {
|
|
||||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
let menu_bar = self.menu_bar.read(cx);
|
|
||||||
let is_selected = menu_bar.selected_index == Some(self.ix);
|
|
||||||
|
|
||||||
div()
|
|
||||||
.id(self.ix)
|
|
||||||
.relative()
|
|
||||||
.child(
|
|
||||||
Button::new("menu")
|
|
||||||
.small()
|
|
||||||
.py_0p5()
|
|
||||||
.compact()
|
|
||||||
.ghost()
|
|
||||||
.label(self.name.clone())
|
|
||||||
.selected(is_selected)
|
|
||||||
.on_mouse_down(MouseButton::Left, |_, window, cx| {
|
|
||||||
// Stop propagation to avoid dragging the window.
|
|
||||||
window.prevent_default();
|
|
||||||
cx.stop_propagation();
|
|
||||||
})
|
|
||||||
.on_click(cx.listener(Self::handle_trigger_click)),
|
|
||||||
)
|
|
||||||
.on_hover(cx.listener(Self::handle_hover))
|
|
||||||
.when(is_selected, |this| {
|
|
||||||
this.child(deferred(
|
|
||||||
anchored()
|
|
||||||
.anchor(gpui::Anchor::TopLeft)
|
|
||||||
.snap_to_window_with_margin(px(8.))
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.size_full()
|
|
||||||
.occlude()
|
|
||||||
.top_1()
|
|
||||||
.child(self.build_popup_menu(window, cx)),
|
|
||||||
),
|
|
||||||
))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,323 +0,0 @@
|
|||||||
use std::cell::RefCell;
|
|
||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
use gpui::prelude::FluentBuilder;
|
|
||||||
use gpui::{
|
|
||||||
Anchor, AnyElement, App, Context, DismissEvent, Element, ElementId, Entity, Focusable,
|
|
||||||
GlobalElementId, Hitbox, HitboxBehavior, InspectorElementId, InteractiveElement, IntoElement,
|
|
||||||
MouseButton, MouseDownEvent, ParentElement, Pixels, Point, StyleRefinement, Styled,
|
|
||||||
Subscription, Window, anchored, deferred, div, px,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::menu::PopupMenu;
|
|
||||||
|
|
||||||
/// A extension trait for adding a context menu to an element.
|
|
||||||
pub trait ContextMenuExt: ParentElement + Styled {
|
|
||||||
/// Add a context menu to the element.
|
|
||||||
///
|
|
||||||
/// This will changed the element to be `relative` positioned, and add a child `ContextMenu` element.
|
|
||||||
/// Because the `ContextMenu` element is positioned `absolute`, it will not affect the layout of the parent element.
|
|
||||||
fn context_menu(
|
|
||||||
self,
|
|
||||||
f: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
|
|
||||||
) -> ContextMenu<Self>
|
|
||||||
where
|
|
||||||
Self: Sized,
|
|
||||||
{
|
|
||||||
// Generate a unique ID based on the element's memory address to ensure
|
|
||||||
// each context menu has its own state and doesn't share with others
|
|
||||||
let id = format!("context-menu-{:p}", &self as *const _);
|
|
||||||
ContextMenu::new(id, self).menu(f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<E: ParentElement + Styled> ContextMenuExt for E {}
|
|
||||||
|
|
||||||
/// A context menu that can be shown on right-click.
|
|
||||||
pub struct ContextMenu<E: ParentElement + Styled + Sized> {
|
|
||||||
id: ElementId,
|
|
||||||
element: Option<E>,
|
|
||||||
#[allow(clippy::type_complexity)]
|
|
||||||
menu: Option<Rc<dyn Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu>>,
|
|
||||||
// This is not in use, just for style refinement forwarding.
|
|
||||||
_ignore_style: StyleRefinement,
|
|
||||||
anchor: Anchor,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<E: ParentElement + Styled> ContextMenu<E> {
|
|
||||||
/// Create a new context menu with the given ID.
|
|
||||||
pub fn new(id: impl Into<ElementId>, element: E) -> Self {
|
|
||||||
Self {
|
|
||||||
id: id.into(),
|
|
||||||
element: Some(element),
|
|
||||||
menu: None,
|
|
||||||
anchor: Anchor::TopLeft,
|
|
||||||
_ignore_style: StyleRefinement::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Build the context menu using the given builder function.
|
|
||||||
#[must_use]
|
|
||||||
fn menu<F>(mut self, builder: F) -> Self
|
|
||||||
where
|
|
||||||
F: Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
|
|
||||||
{
|
|
||||||
self.menu = Some(Rc::new(builder));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn with_element_state<R>(
|
|
||||||
&mut self,
|
|
||||||
id: &GlobalElementId,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut App,
|
|
||||||
f: impl FnOnce(&mut Self, &mut ContextMenuState, &mut Window, &mut App) -> R,
|
|
||||||
) -> R {
|
|
||||||
window.with_optional_element_state::<ContextMenuState, _>(
|
|
||||||
Some(id),
|
|
||||||
|element_state, window| {
|
|
||||||
let mut element_state = element_state.unwrap().unwrap_or_default();
|
|
||||||
let result = f(self, &mut element_state, window, cx);
|
|
||||||
(result, Some(element_state))
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<E: ParentElement + Styled> ParentElement for ContextMenu<E> {
|
|
||||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
|
||||||
if let Some(element) = &mut self.element {
|
|
||||||
element.extend(elements);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<E: ParentElement + Styled> Styled for ContextMenu<E> {
|
|
||||||
fn style(&mut self) -> &mut StyleRefinement {
|
|
||||||
if let Some(element) = &mut self.element {
|
|
||||||
element.style()
|
|
||||||
} else {
|
|
||||||
&mut self._ignore_style
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<E: ParentElement + Styled + IntoElement + 'static> IntoElement for ContextMenu<E> {
|
|
||||||
type Element = Self;
|
|
||||||
|
|
||||||
fn into_element(self) -> Self::Element {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ContextMenuSharedState {
|
|
||||||
menu_view: Option<Entity<PopupMenu>>,
|
|
||||||
open: bool,
|
|
||||||
position: Point<Pixels>,
|
|
||||||
_subscription: Option<Subscription>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ContextMenuState {
|
|
||||||
element: Option<AnyElement>,
|
|
||||||
shared_state: Rc<RefCell<ContextMenuSharedState>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for ContextMenuState {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
element: None,
|
|
||||||
shared_state: Rc::new(RefCell::new(ContextMenuSharedState {
|
|
||||||
menu_view: None,
|
|
||||||
open: false,
|
|
||||||
position: Default::default(),
|
|
||||||
_subscription: None,
|
|
||||||
})),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<E: ParentElement + Styled + IntoElement + 'static> Element for ContextMenu<E> {
|
|
||||||
type PrepaintState = Hitbox;
|
|
||||||
type RequestLayoutState = ContextMenuState;
|
|
||||||
|
|
||||||
fn id(&self) -> Option<ElementId> {
|
|
||||||
Some(self.id.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn request_layout(
|
|
||||||
&mut self,
|
|
||||||
id: Option<&gpui::GlobalElementId>,
|
|
||||||
_: Option<&gpui::InspectorElementId>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut App,
|
|
||||||
) -> (gpui::LayoutId, Self::RequestLayoutState) {
|
|
||||||
let anchor = self.anchor;
|
|
||||||
|
|
||||||
self.with_element_state(
|
|
||||||
id.unwrap(),
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
|this, state: &mut ContextMenuState, window, cx| {
|
|
||||||
let (position, open) = {
|
|
||||||
let shared_state = state.shared_state.borrow();
|
|
||||||
(shared_state.position, shared_state.open)
|
|
||||||
};
|
|
||||||
let menu_view = state.shared_state.borrow().menu_view.clone();
|
|
||||||
let mut menu_element = None;
|
|
||||||
if open {
|
|
||||||
let has_menu_item = menu_view
|
|
||||||
.as_ref()
|
|
||||||
.map(|menu| !menu.read(cx).is_empty())
|
|
||||||
.unwrap_or(false);
|
|
||||||
|
|
||||||
if has_menu_item {
|
|
||||||
menu_element = Some(
|
|
||||||
deferred(
|
|
||||||
anchored().child(
|
|
||||||
div()
|
|
||||||
.w(window.bounds().size.width)
|
|
||||||
.h(window.bounds().size.height)
|
|
||||||
.on_scroll_wheel(|_, _, cx| {
|
|
||||||
cx.stop_propagation();
|
|
||||||
})
|
|
||||||
.child(
|
|
||||||
anchored()
|
|
||||||
.position(position)
|
|
||||||
.snap_to_window_with_margin(px(8.))
|
|
||||||
.anchor(anchor)
|
|
||||||
.when_some(menu_view, |this, menu| {
|
|
||||||
// Focus the menu, so that can be handle the action.
|
|
||||||
if !menu
|
|
||||||
.focus_handle(cx)
|
|
||||||
.contains_focused(window, cx)
|
|
||||||
{
|
|
||||||
menu.focus_handle(cx).focus(window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.child(menu.clone())
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.with_priority(1)
|
|
||||||
.into_any(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut element = this
|
|
||||||
.element
|
|
||||||
.take()
|
|
||||||
.expect("Element should exists.")
|
|
||||||
.children(menu_element)
|
|
||||||
.into_any_element();
|
|
||||||
|
|
||||||
let layout_id = element.request_layout(window, cx);
|
|
||||||
|
|
||||||
(
|
|
||||||
layout_id,
|
|
||||||
ContextMenuState {
|
|
||||||
element: Some(element),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prepaint(
|
|
||||||
&mut self,
|
|
||||||
_: Option<&gpui::GlobalElementId>,
|
|
||||||
_: Option<&InspectorElementId>,
|
|
||||||
bounds: gpui::Bounds<gpui::Pixels>,
|
|
||||||
request_layout: &mut Self::RequestLayoutState,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut App,
|
|
||||||
) -> Self::PrepaintState {
|
|
||||||
if let Some(element) = &mut request_layout.element {
|
|
||||||
element.prepaint(window, cx);
|
|
||||||
}
|
|
||||||
window.insert_hitbox(bounds, HitboxBehavior::Normal)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn paint(
|
|
||||||
&mut self,
|
|
||||||
id: Option<&gpui::GlobalElementId>,
|
|
||||||
_: Option<&InspectorElementId>,
|
|
||||||
_: gpui::Bounds<gpui::Pixels>,
|
|
||||||
request_layout: &mut Self::RequestLayoutState,
|
|
||||||
hitbox: &mut Self::PrepaintState,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut App,
|
|
||||||
) {
|
|
||||||
if let Some(element) = &mut request_layout.element {
|
|
||||||
element.paint(window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Take the builder before setting up element state to avoid borrow issues
|
|
||||||
let builder = self.menu.clone();
|
|
||||||
|
|
||||||
self.with_element_state(
|
|
||||||
id.unwrap(),
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
|_view, state: &mut ContextMenuState, window, _| {
|
|
||||||
let shared_state = state.shared_state.clone();
|
|
||||||
|
|
||||||
let hitbox = hitbox.clone();
|
|
||||||
// When right mouse click, to build content menu, and show it at the mouse position.
|
|
||||||
window.on_mouse_event(move |event: &MouseDownEvent, phase, window, cx| {
|
|
||||||
if phase.bubble()
|
|
||||||
&& event.button == MouseButton::Right
|
|
||||||
&& hitbox.is_hovered(window)
|
|
||||||
{
|
|
||||||
{
|
|
||||||
let mut shared_state = shared_state.borrow_mut();
|
|
||||||
// Clear any existing menu view to allow immediate replacement
|
|
||||||
// Set the new position and open the menu
|
|
||||||
shared_state.menu_view = None;
|
|
||||||
shared_state._subscription = None;
|
|
||||||
shared_state.position = event.position;
|
|
||||||
shared_state.open = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use defer to build the menu in the next frame, avoiding race conditions
|
|
||||||
window.defer(cx, {
|
|
||||||
let shared_state = shared_state.clone();
|
|
||||||
let builder = builder.clone();
|
|
||||||
move |window, cx| {
|
|
||||||
let menu = PopupMenu::build(window, cx, move |menu, window, cx| {
|
|
||||||
let Some(build) = &builder else {
|
|
||||||
return menu;
|
|
||||||
};
|
|
||||||
build(menu, window, cx)
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set up the subscription for dismiss handling
|
|
||||||
let _subscription = window.subscribe(&menu, cx, {
|
|
||||||
let shared_state = shared_state.clone();
|
|
||||||
move |_, _: &DismissEvent, window, _cx| {
|
|
||||||
shared_state.borrow_mut().open = false;
|
|
||||||
window.refresh();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update the shared state with the built menu and subscription
|
|
||||||
{
|
|
||||||
let mut state = shared_state.borrow_mut();
|
|
||||||
state.menu_view = Some(menu.clone());
|
|
||||||
state._subscription = Some(_subscription);
|
|
||||||
window.refresh();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +1,12 @@
|
|||||||
use gpui::App;
|
use gpui::App;
|
||||||
|
|
||||||
mod app_menu_bar;
|
|
||||||
mod context_menu;
|
|
||||||
mod dropdown_menu;
|
mod dropdown_menu;
|
||||||
mod menu_item;
|
mod menu_item;
|
||||||
mod popup_menu;
|
mod popup_menu;
|
||||||
|
|
||||||
pub use app_menu_bar::AppMenuBar;
|
|
||||||
pub use context_menu::{ContextMenu, ContextMenuExt, ContextMenuState};
|
|
||||||
pub use dropdown_menu::DropdownMenu;
|
pub use dropdown_menu::DropdownMenu;
|
||||||
pub use popup_menu::{PopupMenu, PopupMenuItem};
|
pub use popup_menu::{PopupMenu, PopupMenuItem};
|
||||||
|
|
||||||
pub(crate) fn init(cx: &mut App) {
|
pub(crate) fn init(cx: &mut App) {
|
||||||
app_menu_bar::init(cx);
|
|
||||||
popup_menu::init(cx);
|
popup_menu::init(cx);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,12 @@ use gpui::prelude::FluentBuilder;
|
|||||||
use gpui::{
|
use gpui::{
|
||||||
Action, Anchor, AnyElement, App, AppContext, Axis, Bounds, ClickEvent, Context, DismissEvent,
|
Action, Anchor, AnyElement, App, AppContext, Axis, Bounds, ClickEvent, Context, DismissEvent,
|
||||||
Edges, Entity, EventEmitter, FocusHandle, Focusable, Half, InteractiveElement, IntoElement,
|
Edges, Entity, EventEmitter, FocusHandle, Focusable, Half, InteractiveElement, IntoElement,
|
||||||
KeyBinding, MouseDownEvent, OwnedMenuItem, ParentElement, Pixels, Point, Render, ScrollHandle,
|
KeyBinding, MouseDownEvent, ParentElement, Pixels, Point, Render, ScrollHandle, SharedString,
|
||||||
SharedString, StatefulInteractiveElement, Styled, Subscription, WeakEntity, Window, anchored,
|
StatefulInteractiveElement, Styled, Subscription, WeakEntity, Window, anchored, div, px, rems,
|
||||||
div, px, rems,
|
|
||||||
};
|
};
|
||||||
|
use gpui_base::actions::{Cancel, Confirm, SelectDown, SelectLeft, SelectRight, SelectUp};
|
||||||
use theme::{ActiveTheme, Side};
|
use theme::{ActiveTheme, Side};
|
||||||
|
|
||||||
use crate::actions::{Cancel, Confirm, SelectDown, SelectLeft, SelectRight, SelectUp};
|
|
||||||
use crate::kbd::Kbd;
|
use crate::kbd::Kbd;
|
||||||
use crate::menu::menu_item::MenuItemElement;
|
use crate::menu::menu_item::MenuItemElement;
|
||||||
use crate::scroll::ScrollableElement;
|
use crate::scroll::ScrollableElement;
|
||||||
@@ -682,42 +681,6 @@ impl PopupMenu {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn with_menu_items<I>(
|
|
||||||
mut self,
|
|
||||||
items: impl IntoIterator<Item = I>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) -> Self
|
|
||||||
where
|
|
||||||
I: Into<OwnedMenuItem>,
|
|
||||||
{
|
|
||||||
for item in items {
|
|
||||||
match item.into() {
|
|
||||||
OwnedMenuItem::Action {
|
|
||||||
name,
|
|
||||||
action,
|
|
||||||
checked,
|
|
||||||
..
|
|
||||||
} => self = self.menu_with_check(name, checked, action.boxed_clone()),
|
|
||||||
OwnedMenuItem::Separator => {
|
|
||||||
self = self.separator();
|
|
||||||
}
|
|
||||||
OwnedMenuItem::Submenu(submenu) => {
|
|
||||||
self = self.submenu(submenu.name, window, cx, move |menu, window, cx| {
|
|
||||||
menu.with_menu_items(submenu.items.clone(), window, cx)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
OwnedMenuItem::SystemMenu(_) => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.menu_items.len() > 20 {
|
|
||||||
self.scrollable = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn active_submenu(&self) -> Option<Entity<PopupMenu>> {
|
pub(crate) fn active_submenu(&self) -> Option<Entity<PopupMenu>> {
|
||||||
if let Some(ix) = self.selected_index
|
if let Some(ix) = self.selected_index
|
||||||
&& let Some(item) = self.menu_items.get(ix)
|
&& let Some(item) = self.menu_items.get(ix)
|
||||||
|
|||||||
+43
-83
@@ -2,28 +2,19 @@ use std::rc::Rc;
|
|||||||
|
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Animation, AnimationExt as _, AnyElement, App, Bounds, BoxShadow, ClickEvent, Div, FocusHandle,
|
Animation, AnimationExt as _, AnyElement, App, BoxShadow, ClickEvent, Div, FocusHandle,
|
||||||
InteractiveElement, IntoElement, KeyBinding, MouseButton, ParentElement, Pixels, Point,
|
InteractiveElement as _, IntoElement, ParentElement, Pixels, RenderOnce, SharedString,
|
||||||
RenderOnce, SharedString, StyleRefinement, Styled, Window, anchored, div, hsla, point, px,
|
StyleRefinement, Styled, Window, div, hsla, point, px, size,
|
||||||
};
|
};
|
||||||
|
use gpui_base::Dialog;
|
||||||
use instant::Duration;
|
use instant::Duration;
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
|
|
||||||
use crate::actions::{Cancel, Confirm};
|
|
||||||
use crate::animation::cubic_bezier;
|
use crate::animation::cubic_bezier;
|
||||||
use crate::button::{Button, ButtonCustomVariant, ButtonVariant, ButtonVariants as _};
|
use crate::button::{Button, ButtonCustomVariant, ButtonVariant, ButtonVariants as _};
|
||||||
use crate::scroll::ScrollableElement;
|
use crate::scroll::ScrollableElement;
|
||||||
use crate::{IconName, Root, Sizable, StyledExt, WindowExtension, h_flex, v_flex};
|
use crate::{IconName, Root, Sizable, StyledExt, WindowExtension, h_flex, v_flex};
|
||||||
|
|
||||||
const CONTEXT: &str = "Modal";
|
|
||||||
|
|
||||||
pub fn init(cx: &mut App) {
|
|
||||||
cx.bind_keys([
|
|
||||||
KeyBinding::new("escape", Cancel, Some(CONTEXT)),
|
|
||||||
KeyBinding::new("enter", Confirm { secondary: false }, Some(CONTEXT)),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
type OnClose = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>;
|
type OnClose = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>;
|
||||||
type OnOk = Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static>>;
|
type OnOk = Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static>>;
|
||||||
type OnCancel = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static>;
|
type OnCancel = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static>;
|
||||||
@@ -275,6 +266,8 @@ impl Styled for Modal {
|
|||||||
impl RenderOnce for Modal {
|
impl RenderOnce for Modal {
|
||||||
fn render(self, window: &mut Window, cx: &mut App) -> impl gpui::IntoElement {
|
fn render(self, window: &mut Window, cx: &mut App) -> impl gpui::IntoElement {
|
||||||
let layer_ix = self.layer_ix;
|
let layer_ix = self.layer_ix;
|
||||||
|
let is_topmost = layer_ix + 1 == Root::read(window, cx).active_modals.len();
|
||||||
|
let has_footer = self.footer.is_some();
|
||||||
let on_close = self.on_close.clone();
|
let on_close = self.on_close.clone();
|
||||||
let on_ok = self.on_ok.clone();
|
let on_ok = self.on_ok.clone();
|
||||||
let on_cancel = self.on_cancel.clone();
|
let on_cancel = self.on_cancel.clone();
|
||||||
@@ -345,19 +338,16 @@ impl RenderOnce for Modal {
|
|||||||
let radius = cx.theme().radius_lg;
|
let radius = cx.theme().radius_lg;
|
||||||
|
|
||||||
let view_size = window.viewport_size()
|
let view_size = window.viewport_size()
|
||||||
- gpui::size(
|
- size(
|
||||||
window_paddings.left + window_paddings.right,
|
window_paddings.left + window_paddings.right,
|
||||||
window_paddings.top + window_paddings.bottom,
|
window_paddings.top + window_paddings.bottom,
|
||||||
);
|
);
|
||||||
|
|
||||||
let bounds = Bounds {
|
|
||||||
origin: Point::default(),
|
|
||||||
size: view_size,
|
|
||||||
};
|
|
||||||
|
|
||||||
let offset_top = px(layer_ix as f32 * 16.);
|
let offset_top = px(layer_ix as f32 * 16.);
|
||||||
let y = self.margin_top.unwrap_or(view_size.height / 10.) + offset_top;
|
let y = self.margin_top.unwrap_or(view_size.height / 10.) + offset_top;
|
||||||
let x = bounds.center().x - self.width / 2.;
|
let x = view_size.width / 2. - self.width / 2.;
|
||||||
|
let card_top = window_paddings.top + y;
|
||||||
|
let card_left = window_paddings.left + x;
|
||||||
|
|
||||||
let mut padding_right = px(16.);
|
let mut padding_right = px(16.);
|
||||||
let mut padding_left = px(16.);
|
let mut padding_left = px(16.);
|
||||||
@@ -373,35 +363,20 @@ impl RenderOnce for Modal {
|
|||||||
let animation = Animation::new(Duration::from_secs_f64(0.25))
|
let animation = Animation::new(Duration::from_secs_f64(0.25))
|
||||||
.with_easing(cubic_bezier(0.32, 0.72, 0., 1.));
|
.with_easing(cubic_bezier(0.32, 0.72, 0., 1.));
|
||||||
|
|
||||||
anchored()
|
let backdrop = div()
|
||||||
.position(point(window_paddings.left, window_paddings.top))
|
.absolute()
|
||||||
.snap_to_window()
|
.top(window_paddings.top)
|
||||||
.child(
|
.left(window_paddings.left)
|
||||||
div()
|
|
||||||
.id("modal")
|
|
||||||
.w(view_size.width)
|
.w(view_size.width)
|
||||||
.h(view_size.height)
|
.h(view_size.height)
|
||||||
.when(self.overlay_visible, |this| {
|
.when(self.overlay_visible, |this| {
|
||||||
this.occlude().bg(cx.theme().overlay)
|
this.occlude().bg(cx.theme().overlay)
|
||||||
})
|
})
|
||||||
.when(self.overlay_closable, |this| {
|
.with_animation("fade-in", animation.clone(), move |this, delta| {
|
||||||
// Only the last modal owns the `mouse down - close modal` event.
|
this.opacity(delta)
|
||||||
if (self.layer_ix + 1) != Root::read(window, cx).active_modals.len() {
|
});
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.on_mouse_down(MouseButton::Left, {
|
let card = v_flex()
|
||||||
let on_cancel = on_cancel.clone();
|
|
||||||
let on_close = on_close.clone();
|
|
||||||
move |_, window, cx| {
|
|
||||||
on_cancel(&ClickEvent::default(), window, cx);
|
|
||||||
on_close(&ClickEvent::default(), window, cx);
|
|
||||||
window.close_modal(cx);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.id(layer_ix)
|
.id(layer_ix)
|
||||||
.bg(cx.theme().background)
|
.bg(cx.theme().background)
|
||||||
.border_1()
|
.border_1()
|
||||||
@@ -409,45 +384,13 @@ impl RenderOnce for Modal {
|
|||||||
.rounded(radius)
|
.rounded(radius)
|
||||||
.when(cx.theme().shadow, |this| this.shadow_xl())
|
.when(cx.theme().shadow, |this| this.shadow_xl())
|
||||||
.min_h_24()
|
.min_h_24()
|
||||||
.key_context(CONTEXT)
|
|
||||||
.track_focus(&self.focus_handle)
|
|
||||||
.refine_style(&self.style)
|
.refine_style(&self.style)
|
||||||
.when(self.keyboard, |this| {
|
|
||||||
this.on_action({
|
|
||||||
let on_cancel = on_cancel.clone();
|
|
||||||
let on_close = on_close.clone();
|
|
||||||
move |_: &Cancel, window, cx| {
|
|
||||||
// FIXME:
|
|
||||||
//
|
|
||||||
// Here some Modal have no focus_handle, so it will not work will Escape key.
|
|
||||||
// But by now, we `cx.close_modal()` going to close the last active model, so the Escape is unexpected to work.
|
|
||||||
on_cancel(&ClickEvent::default(), window, cx);
|
|
||||||
on_close(&ClickEvent::default(), window, cx);
|
|
||||||
window.close_modal(cx);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.on_action({
|
|
||||||
let on_ok = on_ok.clone();
|
|
||||||
let on_close = on_close.clone();
|
|
||||||
let has_footer = self.footer.is_some();
|
|
||||||
move |_: &Confirm, window, cx| {
|
|
||||||
if let Some(on_ok) = &on_ok {
|
|
||||||
if on_ok(&ClickEvent::default(), window, cx) {
|
|
||||||
on_close(&ClickEvent::default(), window, cx);
|
|
||||||
window.close_modal(cx);
|
|
||||||
}
|
|
||||||
} else if has_footer {
|
|
||||||
window.close_modal(cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
// There style is high priority, can't be overridden.
|
// There style is high priority, can't be overridden.
|
||||||
.absolute()
|
.absolute()
|
||||||
.occlude()
|
.occlude()
|
||||||
.relative()
|
.relative()
|
||||||
.left(x)
|
.left(card_left)
|
||||||
.top(y)
|
.top(card_top)
|
||||||
.w(self.width)
|
.w(self.width)
|
||||||
.when_some(self.max_width, |this, w| this.max_w(w))
|
.when_some(self.max_width, |this, w| this.max_w(w))
|
||||||
.child(
|
.child(
|
||||||
@@ -463,6 +406,9 @@ impl RenderOnce for Modal {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.when(self.show_close, |this| {
|
.when(self.show_close, |this| {
|
||||||
|
let on_cancel = on_cancel.clone();
|
||||||
|
let on_close = on_close.clone();
|
||||||
|
|
||||||
this.child(
|
this.child(
|
||||||
Button::new("close")
|
Button::new("close")
|
||||||
.icon(IconName::CloseCircleFill)
|
.icon(IconName::CloseCircleFill)
|
||||||
@@ -512,7 +458,7 @@ impl RenderOnce for Modal {
|
|||||||
.children(footer(render_ok, render_cancel, window, cx)),
|
.children(footer(render_ok, render_cancel, window, cx)),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.with_animation("slide-down", animation.clone(), move |this, delta| {
|
.with_animation("slide-down", animation, move |this, delta| {
|
||||||
let y_offset = px(0.) + delta * px(30.);
|
let y_offset = px(0.) + delta * px(30.);
|
||||||
// This is equivalent to `shadow_xl` with an extra opacity.
|
// This is equivalent to `shadow_xl` with an extra opacity.
|
||||||
let shadow = vec![
|
let shadow = vec![
|
||||||
@@ -531,10 +477,24 @@ impl RenderOnce for Modal {
|
|||||||
inset: false,
|
inset: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
this.top(y + y_offset).shadow(shadow)
|
this.top(card_top + y_offset).shadow(shadow)
|
||||||
}),
|
});
|
||||||
)
|
|
||||||
.with_animation("fade-in", animation, move |this, delta| this.opacity(delta)),
|
Dialog::new(cx)
|
||||||
)
|
.layer(layer_ix, is_topmost)
|
||||||
|
.focus_handle(self.focus_handle.clone())
|
||||||
|
.close_on_escape(self.keyboard)
|
||||||
|
.close_on_backdrop_press(self.overlay_closable)
|
||||||
|
.on_ok(move |event, window, cx| match &on_ok {
|
||||||
|
Some(on_ok) => on_ok(event, window, cx),
|
||||||
|
None => has_footer,
|
||||||
|
})
|
||||||
|
.on_cancel(move |event, window, cx| on_cancel(event, window, cx))
|
||||||
|
.on_close(move |event, window, cx| {
|
||||||
|
on_close(event, window, cx);
|
||||||
|
window.close_modal(cx);
|
||||||
|
})
|
||||||
|
.backdrop(backdrop)
|
||||||
|
.popup(card)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+176
-97
@@ -1,7 +1,7 @@
|
|||||||
use std::any::TypeId;
|
use std::any::TypeId;
|
||||||
use std::collections::{HashMap, VecDeque};
|
use std::collections::HashMap;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use instant::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
@@ -10,12 +10,25 @@ use gpui::{
|
|||||||
ParentElement as _, Render, SharedString, StatefulInteractiveElement, StyleRefinement, Styled,
|
ParentElement as _, Render, SharedString, StatefulInteractiveElement, StyleRefinement, Styled,
|
||||||
Subscription, Window, div, px, relative,
|
Subscription, Window, div, px, relative,
|
||||||
};
|
};
|
||||||
|
use gpui_base::{
|
||||||
|
Toast as BaseToast, ToastManager, ToastMotion, ToastOptions, ToastStack, ToastStackState,
|
||||||
|
ToastTransitionStatus,
|
||||||
|
};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
|
|
||||||
use crate::animation::cubic_bezier;
|
use crate::animation::cubic_bezier;
|
||||||
use crate::button::{Button, ButtonVariants as _};
|
use crate::button::{Button, ButtonVariants as _};
|
||||||
use crate::{Icon, IconName, Sizable as _, Size, StyledExt, h_flex, v_flex};
|
use crate::{Icon, IconName, Sizable as _, Size, StyledExt, h_flex, v_flex};
|
||||||
|
|
||||||
|
/// How often the notification lifecycle clock is sampled.
|
||||||
|
const ADVANCE_INTERVAL: Duration = Duration::from_millis(50);
|
||||||
|
|
||||||
|
/// How long a notification stays before it hides itself.
|
||||||
|
const AUTOHIDE_DURATION: Duration = Duration::from_secs(5);
|
||||||
|
|
||||||
|
/// Request by a notification to be dismissed; the list owns the transition.
|
||||||
|
struct DismissRequest;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Default)]
|
#[derive(Debug, Clone, Copy, Default)]
|
||||||
pub enum NotificationKind {
|
pub enum NotificationKind {
|
||||||
#[default]
|
#[default]
|
||||||
@@ -79,7 +92,7 @@ pub struct Notification {
|
|||||||
action_builder: Option<Rc<dyn Fn(&mut Self, &mut Window, &mut Context<Self>) -> Button>>,
|
action_builder: Option<Rc<dyn Fn(&mut Self, &mut Window, &mut Context<Self>) -> Button>>,
|
||||||
content_builder: Option<Rc<dyn Fn(&mut Self, &mut Window, &mut Context<Self>) -> AnyElement>>,
|
content_builder: Option<Rc<dyn Fn(&mut Self, &mut Window, &mut Context<Self>) -> AnyElement>>,
|
||||||
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
|
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
|
||||||
closing: bool,
|
transition_status: ToastTransitionStatus,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<String> for Notification {
|
impl From<String> for Notification {
|
||||||
@@ -133,7 +146,7 @@ impl Notification {
|
|||||||
action_builder: None,
|
action_builder: None,
|
||||||
content_builder: None,
|
content_builder: None,
|
||||||
on_click: None,
|
on_click: None,
|
||||||
closing: false,
|
transition_status: ToastTransitionStatus::Starting,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,29 +251,29 @@ impl Notification {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Dismiss the notification.
|
/// Dismiss the notification.
|
||||||
pub fn dismiss(&mut self, _: &mut Window, cx: &mut Context<Self>) {
|
pub fn dismiss(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||||
if self.closing {
|
cx.emit(DismissRequest);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
self.closing = true;
|
|
||||||
|
/// Begin the exit transition, driven by the notification list.
|
||||||
|
pub(crate) fn begin_close(&mut self, cx: &mut Context<Self>) {
|
||||||
|
if self.transition_status != ToastTransitionStatus::Ending {
|
||||||
|
self.transition_status = ToastTransitionStatus::Ending;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
|
||||||
// Dismiss the notification after 0.15s to show the animation.
|
|
||||||
cx.spawn(async move |view, cx| {
|
|
||||||
cx.background_executor()
|
|
||||||
.timer(Duration::from_secs_f32(0.15))
|
|
||||||
.await;
|
|
||||||
|
|
||||||
cx.update(|cx| {
|
|
||||||
if let Some(view) = view.upgrade() {
|
|
||||||
view.update(cx, |view, cx| {
|
|
||||||
view.closing = false;
|
|
||||||
cx.emit(DismissEvent);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
})
|
|
||||||
.detach();
|
/// Mark the enter transition as finished, driven by the notification list.
|
||||||
|
pub(crate) fn complete_enter(&mut self, cx: &mut Context<Self>) {
|
||||||
|
if self.transition_status == ToastTransitionStatus::Starting {
|
||||||
|
self.transition_status = ToastTransitionStatus::Present;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finish the exit transition, driven by the notification list.
|
||||||
|
pub(crate) fn complete_close(&mut self, cx: &mut Context<Self>) {
|
||||||
|
cx.emit(DismissEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the content of the notification.
|
/// Set the content of the notification.
|
||||||
@@ -280,6 +293,7 @@ impl Default for Notification {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl EventEmitter<DismissEvent> for Notification {}
|
impl EventEmitter<DismissEvent> for Notification {}
|
||||||
|
impl EventEmitter<DismissRequest> for Notification {}
|
||||||
|
|
||||||
impl FluentBuilder for Notification {}
|
impl FluentBuilder for Notification {}
|
||||||
|
|
||||||
@@ -319,17 +333,19 @@ impl Render for Notification {
|
|||||||
_ => cx.theme().text,
|
_ => cx.theme().text,
|
||||||
};
|
};
|
||||||
|
|
||||||
let closing = self.closing;
|
let transition_status = self.transition_status;
|
||||||
|
let closing = transition_status == ToastTransitionStatus::Ending;
|
||||||
let has_title = self.title.is_some();
|
let has_title = self.title.is_some();
|
||||||
let only_message = !has_title && content.is_none() && action.is_none();
|
let only_message = !has_title && content.is_none() && action.is_none();
|
||||||
let placement = cx.theme().notification.placement;
|
let placement = cx.theme().notification.placement;
|
||||||
|
|
||||||
h_flex()
|
BaseToast::new("notification")
|
||||||
.id("notification")
|
.transition_status(transition_status)
|
||||||
|
.h_flex()
|
||||||
.group("")
|
.group("")
|
||||||
.occlude()
|
.occlude()
|
||||||
.relative()
|
.relative()
|
||||||
.w_112()
|
.w_full()
|
||||||
.border_1()
|
.border_1()
|
||||||
.border_color(cx.theme().border)
|
.border_color(cx.theme().border)
|
||||||
.bg(background)
|
.bg(background)
|
||||||
@@ -455,10 +471,13 @@ impl Render for Notification {
|
|||||||
/// A list of notifications.
|
/// A list of notifications.
|
||||||
pub struct NotificationList {
|
pub struct NotificationList {
|
||||||
/// Notifications that will be auto hidden.
|
/// Notifications that will be auto hidden.
|
||||||
pub(crate) notifications: VecDeque<Entity<Notification>>,
|
pub(crate) notifications: ToastManager<NotificationId, Entity<Notification>>,
|
||||||
|
|
||||||
/// Whether the notification list is expanded.
|
/// Measured geometry and interaction state of the visible stack.
|
||||||
expanded: bool,
|
stack_state: ToastStackState,
|
||||||
|
|
||||||
|
/// Whether the lifecycle clock is running. The loop clears it as it exits.
|
||||||
|
is_advancing: bool,
|
||||||
|
|
||||||
/// Subscriptions
|
/// Subscriptions
|
||||||
_subscriptions: HashMap<NotificationId, Subscription>,
|
_subscriptions: HashMap<NotificationId, Subscription>,
|
||||||
@@ -467,12 +486,64 @@ pub struct NotificationList {
|
|||||||
impl NotificationList {
|
impl NotificationList {
|
||||||
pub fn new(_window: &mut Window, _cx: &mut Context<Self>) -> Self {
|
pub fn new(_window: &mut Window, _cx: &mut Context<Self>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
notifications: VecDeque::new(),
|
notifications: ToastManager::new(ToastMotion::default()),
|
||||||
expanded: false,
|
stack_state: ToastStackState::default(),
|
||||||
|
is_advancing: false,
|
||||||
_subscriptions: HashMap::new(),
|
_subscriptions: HashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Tick the toast lifecycle until the last notification is unmounted.
|
||||||
|
///
|
||||||
|
/// The stack expansion is sampled here because it reaches the list through
|
||||||
|
/// no event, and an idle window should arm no timer.
|
||||||
|
fn start_advancing(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if self.is_advancing {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.is_advancing = true;
|
||||||
|
cx.spawn_in(window, async move |view, cx| {
|
||||||
|
loop {
|
||||||
|
cx.background_executor().timer(ADVANCE_INTERVAL).await;
|
||||||
|
let running = view.update(cx, |view, cx| {
|
||||||
|
view.advance(cx);
|
||||||
|
view.is_advancing = !view.notifications.is_empty();
|
||||||
|
view.is_advancing
|
||||||
|
});
|
||||||
|
if !matches!(running, Ok(true)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn advance(&mut self, cx: &mut Context<Self>) {
|
||||||
|
let changes = self.notifications.advance(
|
||||||
|
cx.background_executor().now(),
|
||||||
|
self.stack_state.is_expanded(),
|
||||||
|
);
|
||||||
|
|
||||||
|
for id in changes.presented {
|
||||||
|
if let Some(note) = self.notifications.get(&id) {
|
||||||
|
note.update(cx, |note, cx| note.complete_enter(cx));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for id in changes.ending {
|
||||||
|
if let Some(note) = self.notifications.get(&id) {
|
||||||
|
note.update(cx, |note, cx| note.begin_close(cx));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (id, note) in changes.removed {
|
||||||
|
self._subscriptions.remove(&id);
|
||||||
|
note.update(cx, |note, cx| note.complete_close(cx));
|
||||||
|
}
|
||||||
|
|
||||||
|
if changes.changed {
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn push(
|
pub fn push(
|
||||||
&mut self,
|
&mut self,
|
||||||
notification: impl Into<Notification>,
|
notification: impl Into<Notification>,
|
||||||
@@ -483,102 +554,110 @@ impl NotificationList {
|
|||||||
let id = notification.id.clone();
|
let id = notification.id.clone();
|
||||||
let autohide = notification.autohide;
|
let autohide = notification.autohide;
|
||||||
|
|
||||||
// Remove the notification by id, for keep unique.
|
|
||||||
self.notifications.retain(|note| note.read(cx).id != id);
|
|
||||||
|
|
||||||
let notification = cx.new(|_| notification);
|
let notification = cx.new(|_| notification);
|
||||||
|
|
||||||
|
let dismiss_id = id.clone();
|
||||||
self._subscriptions.insert(
|
self._subscriptions.insert(
|
||||||
id.clone(),
|
id.clone(),
|
||||||
cx.subscribe(¬ification, move |view, _, _: &DismissEvent, cx| {
|
cx.subscribe(¬ification, move |view, _, _: &DismissRequest, cx| {
|
||||||
view.notifications.retain(|note| id != note.read(cx).id);
|
if view
|
||||||
view._subscriptions.remove(&id);
|
.notifications
|
||||||
|
.dismiss(&dismiss_id, cx.background_executor().now())
|
||||||
|
&& let Some(note) = view.notifications.get(&dismiss_id)
|
||||||
|
{
|
||||||
|
note.update(cx, |note, cx| note.begin_close(cx));
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
self.notifications.push_back(notification.clone());
|
self.notifications.push(
|
||||||
|
id,
|
||||||
if autohide {
|
notification,
|
||||||
// Sleep for 5 seconds to autohide the notification
|
ToastOptions {
|
||||||
cx.spawn_in(window, async move |_this, cx| {
|
timeout: autohide.then_some(AUTOHIDE_DURATION),
|
||||||
cx.background_executor().timer(Duration::from_secs(5)).await;
|
},
|
||||||
|
cx.background_executor().now(),
|
||||||
if let Err(err) =
|
);
|
||||||
notification.update_in(cx, |note, window, cx| note.dismiss(window, cx))
|
|
||||||
{
|
|
||||||
log::error!("failed to auto hide notification: {:?}", err);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
self.start_advancing(window, cx);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn close(
|
pub(crate) fn close(
|
||||||
&mut self,
|
&mut self,
|
||||||
id: impl Into<NotificationId>,
|
id: impl Into<NotificationId>,
|
||||||
window: &mut Window,
|
_window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
let id: NotificationId = id.into();
|
let id: NotificationId = id.into();
|
||||||
if let Some(n) = self.notifications.iter().find(|n| n.read(cx).id == id) {
|
if self
|
||||||
n.update(cx, |note, cx| note.dismiss(window, cx))
|
.notifications
|
||||||
|
.dismiss(&id, cx.background_executor().now())
|
||||||
|
&& let Some(note) = self.notifications.get(&id)
|
||||||
|
{
|
||||||
|
note.update(cx, |note, cx| note.begin_close(cx));
|
||||||
}
|
}
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clear(&mut self, _: &mut Window, cx: &mut Context<Self>) {
|
pub fn clear(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||||
self.notifications.clear();
|
for id in self
|
||||||
|
.notifications
|
||||||
|
.dismiss_all(cx.background_executor().now())
|
||||||
|
{
|
||||||
|
if let Some(note) = self.notifications.get(&id) {
|
||||||
|
note.update(cx, |note, cx| note.begin_close(cx));
|
||||||
|
}
|
||||||
|
}
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn notifications(&self) -> Vec<Entity<Notification>> {
|
pub fn notifications(&self) -> Vec<Entity<Notification>> {
|
||||||
self.notifications.iter().cloned().collect()
|
self.notifications
|
||||||
|
.iter()
|
||||||
|
.map(|(_, note, _)| note.clone())
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for NotificationList {
|
impl Render for NotificationList {
|
||||||
fn render(
|
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
&mut self,
|
|
||||||
window: &mut gpui::Window,
|
|
||||||
cx: &mut gpui::Context<Self>,
|
|
||||||
) -> impl IntoElement {
|
|
||||||
let size = window.viewport_size();
|
let size = window.viewport_size();
|
||||||
let items = self.notifications.iter().rev().take(10).rev().cloned();
|
let settings = &cx.theme().notification;
|
||||||
|
let (placement, margins, max_items) = (
|
||||||
|
settings.placement,
|
||||||
|
settings.margins.clone(),
|
||||||
|
settings.max_items,
|
||||||
|
);
|
||||||
|
|
||||||
let placement = cx.theme().notification.placement;
|
let items = self
|
||||||
let margins = &cx.theme().notification.margins;
|
.notifications
|
||||||
|
.visible(max_items)
|
||||||
|
.map(|(id, note, _)| (id.clone(), note.clone()))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
v_flex()
|
let stack = items
|
||||||
.id("notification-list")
|
.into_iter()
|
||||||
|
.fold(
|
||||||
|
ToastStack::new("notification-list", self.stack_state.clone()),
|
||||||
|
|stack, (id, note)| stack.item(format!("{id:?}"), note),
|
||||||
|
)
|
||||||
|
.placement(placement)
|
||||||
|
.v_flex()
|
||||||
|
.w_112()
|
||||||
.max_h(size.height)
|
.max_h(size.height)
|
||||||
.pt(margins.top)
|
.absolute()
|
||||||
.pb(margins.bottom)
|
.map(|this| match placement {
|
||||||
.gap_3()
|
Anchor::TopLeft => this.top(margins.top).left(margins.left),
|
||||||
.when(
|
Anchor::TopRight => this.top(margins.top).right(margins.right),
|
||||||
matches!(placement, Anchor::TopRight),
|
Anchor::TopCenter => this.top(margins.top).left_0().right_0().mx_auto(),
|
||||||
|this| this.pr(margins.right), // ignore left
|
Anchor::BottomLeft => this.bottom(margins.bottom).left(margins.left),
|
||||||
)
|
Anchor::BottomRight => this.bottom(margins.bottom).right(margins.right),
|
||||||
.when(
|
Anchor::BottomCenter => this.bottom(margins.bottom).left_0().right_0().mx_auto(),
|
||||||
matches!(placement, Anchor::TopLeft),
|
Anchor::LeftCenter => this.left(margins.left).top_0().bottom_0().my_auto(),
|
||||||
|this| this.pl(margins.left), // ignore right
|
Anchor::RightCenter => this.right(margins.right).top_0().bottom_0().my_auto(),
|
||||||
)
|
});
|
||||||
.when(
|
|
||||||
matches!(placement, Anchor::BottomLeft),
|
div().size_full().child(stack)
|
||||||
|this| this.flex_col_reverse().pl(margins.left), // ignore right
|
|
||||||
)
|
|
||||||
.when(
|
|
||||||
matches!(placement, Anchor::BottomRight),
|
|
||||||
|this| this.flex_col_reverse().pr(margins.right), // ignore left
|
|
||||||
)
|
|
||||||
.when(matches!(placement, Anchor::BottomCenter), |this| {
|
|
||||||
this.flex_col_reverse()
|
|
||||||
})
|
|
||||||
.on_hover(cx.listener(|view, hovered, _, cx| {
|
|
||||||
view.expanded = *hovered;
|
|
||||||
cx.notify()
|
|
||||||
}))
|
|
||||||
.children(items)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+29
-227
@@ -2,20 +2,13 @@ use std::rc::Rc;
|
|||||||
|
|
||||||
use gpui::prelude::FluentBuilder as _;
|
use gpui::prelude::FluentBuilder as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Anchor, AnyElement, App, Bounds, Context, Deferred, DismissEvent, Div, ElementId, EventEmitter,
|
Anchor, AnyElement, App, Context, Div, ElementId, FocusHandle, InteractiveElement as _,
|
||||||
FocusHandle, Focusable, InteractiveElement as _, IntoElement, KeyBinding, MouseButton,
|
IntoElement, MouseButton, ParentElement, RenderOnce, Stateful, StyleRefinement, Styled, Window,
|
||||||
ParentElement, Pixels, Point, Render, RenderOnce, Stateful, StyleRefinement, Styled,
|
|
||||||
Subscription, Window, anchored, deferred, div, px,
|
|
||||||
};
|
};
|
||||||
|
use gpui_base::Popover as BasePopover;
|
||||||
|
pub use gpui_base::PopoverState;
|
||||||
|
|
||||||
use crate::actions::Cancel;
|
use crate::{Selectable, StyledExt as _, v_flex};
|
||||||
use crate::{ElementExt, Selectable, StyledExt as _, v_flex};
|
|
||||||
|
|
||||||
const CONTEXT: &str = "Popover";
|
|
||||||
|
|
||||||
pub(crate) fn init(cx: &mut App) {
|
|
||||||
cx.bind_keys([KeyBinding::new("escape", Cancel, Some(CONTEXT))])
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A popover element that can be triggered by a button or any other element.
|
/// A popover element that can be triggered by a button or any other element.
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
@@ -173,28 +166,6 @@ impl Popover {
|
|||||||
self.tracked_focus_handle = Some(handle.clone());
|
self.tracked_focus_handle = Some(handle.clone());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn resolved_corner(anchor: Anchor, trigger_bounds: Bounds<Pixels>) -> Point<Pixels> {
|
|
||||||
match anchor {
|
|
||||||
Anchor::TopLeft => trigger_bounds.origin,
|
|
||||||
Anchor::TopCenter => trigger_bounds.top_center(),
|
|
||||||
Anchor::TopRight => trigger_bounds.top_right(),
|
|
||||||
Anchor::BottomLeft => Point {
|
|
||||||
x: trigger_bounds.origin.x,
|
|
||||||
y: trigger_bounds.origin.y - trigger_bounds.size.height,
|
|
||||||
},
|
|
||||||
Anchor::BottomCenter => Point {
|
|
||||||
x: trigger_bounds.top_center().x,
|
|
||||||
y: trigger_bounds.origin.y - trigger_bounds.size.height,
|
|
||||||
},
|
|
||||||
Anchor::BottomRight => Point {
|
|
||||||
x: trigger_bounds.top_right().x,
|
|
||||||
y: trigger_bounds.origin.y - trigger_bounds.size.height,
|
|
||||||
},
|
|
||||||
// Fallback for LeftCenter/RightCenter – adjust as needed.
|
|
||||||
_ => trigger_bounds.origin,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ParentElement for Popover {
|
impl ParentElement for Popover {
|
||||||
@@ -209,119 +180,7 @@ impl Styled for Popover {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct PopoverState {
|
|
||||||
focus_handle: FocusHandle,
|
|
||||||
pub(crate) tracked_focus_handle: Option<FocusHandle>,
|
|
||||||
trigger_bounds: Bounds<Pixels>,
|
|
||||||
open: bool,
|
|
||||||
#[allow(clippy::type_complexity)]
|
|
||||||
on_open_change: Option<Rc<dyn Fn(&bool, &mut Window, &mut App)>>,
|
|
||||||
|
|
||||||
_dismiss_subscription: Option<Subscription>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PopoverState {
|
|
||||||
pub fn new(default_open: bool, cx: &mut App) -> Self {
|
|
||||||
Self {
|
|
||||||
focus_handle: cx.focus_handle(),
|
|
||||||
tracked_focus_handle: None,
|
|
||||||
trigger_bounds: Bounds::default(),
|
|
||||||
open: default_open,
|
|
||||||
on_open_change: None,
|
|
||||||
_dismiss_subscription: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if the popover is open.
|
|
||||||
pub fn is_open(&self) -> bool {
|
|
||||||
self.open
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Dismiss the popover if it is open.
|
|
||||||
pub fn dismiss(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
if self.open {
|
|
||||||
self.toggle_open(window, cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Open the popover if it is closed.
|
|
||||||
pub fn show(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
if !self.open {
|
|
||||||
self.toggle_open(window, cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn toggle_open(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
self.open = !self.open;
|
|
||||||
if self.open {
|
|
||||||
let state = cx.entity();
|
|
||||||
let focus_handle = if let Some(tracked_focus_handle) = self.tracked_focus_handle.clone()
|
|
||||||
{
|
|
||||||
tracked_focus_handle
|
|
||||||
} else {
|
|
||||||
self.focus_handle.clone()
|
|
||||||
};
|
|
||||||
focus_handle.focus(window, cx);
|
|
||||||
|
|
||||||
self._dismiss_subscription =
|
|
||||||
Some(
|
|
||||||
window.subscribe(&cx.entity(), cx, move |_, _: &DismissEvent, window, cx| {
|
|
||||||
state.update(cx, |state, cx| {
|
|
||||||
state.dismiss(window, cx);
|
|
||||||
});
|
|
||||||
window.refresh();
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
self._dismiss_subscription = None;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(callback) = self.on_open_change.as_ref() {
|
|
||||||
callback(&self.open, window, cx);
|
|
||||||
}
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_action_cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
self.dismiss(window, cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Focusable for PopoverState {
|
|
||||||
fn focus_handle(&self, _: &App) -> FocusHandle {
|
|
||||||
self.focus_handle.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for PopoverState {
|
|
||||||
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
div()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EventEmitter<DismissEvent> for PopoverState {}
|
|
||||||
|
|
||||||
impl Popover {
|
impl Popover {
|
||||||
pub(crate) fn render_popover<E>(
|
|
||||||
anchor: Anchor,
|
|
||||||
trigger_bounds: Bounds<Pixels>,
|
|
||||||
content: E,
|
|
||||||
_: &mut Window,
|
|
||||||
_: &mut App,
|
|
||||||
) -> Deferred
|
|
||||||
where
|
|
||||||
E: IntoElement + 'static,
|
|
||||||
{
|
|
||||||
deferred(
|
|
||||||
anchored()
|
|
||||||
.snap_to_window_with_margin(px(8.))
|
|
||||||
.anchor(anchor)
|
|
||||||
.position(Self::resolved_corner(anchor, trigger_bounds))
|
|
||||||
.child(div().relative().child(content)),
|
|
||||||
)
|
|
||||||
.with_priority(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn render_popover_content(
|
pub(crate) fn render_popover_content(
|
||||||
anchor: Anchor,
|
anchor: Anchor,
|
||||||
appearance: bool,
|
appearance: bool,
|
||||||
@@ -342,91 +201,34 @@ impl Popover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl RenderOnce for Popover {
|
impl RenderOnce for Popover {
|
||||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
|
||||||
let force_open = self.open;
|
let anchor = self.anchor;
|
||||||
let default_open = self.default_open;
|
let appearance = self.appearance;
|
||||||
let tracked_focus_handle = self.tracked_focus_handle.clone();
|
let style = self.style;
|
||||||
let state = window.use_keyed_state(self.id.clone(), cx, |_, cx| {
|
let children = self.children;
|
||||||
PopoverState::new(default_open, cx)
|
let content = self.content;
|
||||||
});
|
|
||||||
|
|
||||||
state.update(cx, |state, _| {
|
BasePopover::new(self.id)
|
||||||
if let Some(tracked_focus_handle) = tracked_focus_handle {
|
.anchor(anchor)
|
||||||
state.tracked_focus_handle = Some(tracked_focus_handle);
|
.mouse_button(self.mouse_button)
|
||||||
}
|
.default_open(self.default_open)
|
||||||
state.on_open_change = self.on_open_change.clone();
|
.overlay_closable(self.overlay_closable)
|
||||||
if let Some(force_open) = force_open {
|
.content(move |state, window, cx| {
|
||||||
state.open = force_open;
|
Self::render_popover_content(anchor, appearance, window, cx)
|
||||||
}
|
.when_some(content, |this, content| {
|
||||||
});
|
this.child((content)(state, window, cx))
|
||||||
|
|
||||||
let open = state.read(cx).open;
|
|
||||||
let focus_handle = state.read(cx).focus_handle.clone();
|
|
||||||
let trigger_bounds = state.read(cx).trigger_bounds;
|
|
||||||
|
|
||||||
let Some(trigger) = self.trigger else {
|
|
||||||
return div().id("empty");
|
|
||||||
};
|
|
||||||
|
|
||||||
let parent_view_id = window.current_view();
|
|
||||||
|
|
||||||
let el = div()
|
|
||||||
.id(self.id)
|
|
||||||
.child((trigger)(open, window, cx))
|
|
||||||
.on_mouse_down(self.mouse_button, {
|
|
||||||
let state = state.clone();
|
|
||||||
move |_, window, cx| {
|
|
||||||
cx.stop_propagation();
|
|
||||||
state.update(cx, |state, cx| {
|
|
||||||
// We force set open to false to toggle it correctly.
|
|
||||||
// Because if the mouse down out will toggle open first.
|
|
||||||
state.open = open;
|
|
||||||
state.toggle_open(window, cx);
|
|
||||||
});
|
|
||||||
cx.notify(parent_view_id);
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.on_prepaint({
|
.children(children)
|
||||||
let state = state.clone();
|
.refine_style(&style)
|
||||||
move |bounds, _, cx| {
|
|
||||||
state.update(cx, |state, _| {
|
|
||||||
state.trigger_bounds = bounds;
|
|
||||||
})
|
})
|
||||||
}
|
.when_some(self.trigger, |this, trigger| this.trigger_with(trigger))
|
||||||
});
|
.when_some(self.open, |this, open| this.open(open))
|
||||||
|
.when_some(self.tracked_focus_handle, |this, handle| {
|
||||||
if !open {
|
this.track_focus(&handle)
|
||||||
return el;
|
|
||||||
}
|
|
||||||
|
|
||||||
let popover_content =
|
|
||||||
Self::render_popover_content(self.anchor, self.appearance, window, cx)
|
|
||||||
.track_focus(&focus_handle)
|
|
||||||
.key_context(CONTEXT)
|
|
||||||
.on_action(window.listener_for(&state, PopoverState::on_action_cancel))
|
|
||||||
.when_some(self.content, |this, content| {
|
|
||||||
this.child(state.update(cx, |state, cx| (content)(state, window, cx)))
|
|
||||||
})
|
})
|
||||||
.children(self.children)
|
.when_some(self.on_open_change, |this, callback| {
|
||||||
.when(self.overlay_closable, |this| {
|
this.on_open_change(move |open, window, cx| callback(open, window, cx))
|
||||||
this.on_mouse_down_out({
|
|
||||||
let state = state.clone();
|
|
||||||
move |_, window, cx| {
|
|
||||||
state.update(cx, |state, cx| {
|
|
||||||
state.dismiss(window, cx);
|
|
||||||
});
|
|
||||||
cx.notify(parent_view_id);
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
})
|
.into_any_element()
|
||||||
.refine_style(&self.style);
|
|
||||||
|
|
||||||
el.child(Self::render_popover(
|
|
||||||
self.anchor,
|
|
||||||
trigger_bounds,
|
|
||||||
popover_content,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-286
@@ -1,294 +1,34 @@
|
|||||||
use std::ops::Range;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use gpui::{
|
use gpui::prelude::FluentBuilder as _;
|
||||||
Along, App, Axis, Bounds, Context, ElementId, EventEmitter, IsZero, Pixels, Window, px,
|
use gpui::{App, InteractiveElement as _, IntoElement, Pixels, Styled as _, Window, div, px};
|
||||||
|
pub(crate) use gpui_base::resize_handle;
|
||||||
|
pub use gpui_base::{
|
||||||
|
ResizablePanel, ResizablePanelEvent, ResizablePanelGroup, ResizableState, h_resizable,
|
||||||
|
resizable_panel, v_resizable,
|
||||||
};
|
};
|
||||||
|
use gpui_base::{ResizeHandleContext, ResizeHandleRenderer};
|
||||||
|
use theme::{ActiveTheme as _, AxisExt as _};
|
||||||
|
|
||||||
mod panel;
|
const HANDLE_SIZE: Pixels = px(1.);
|
||||||
mod resize_handle;
|
|
||||||
pub use panel::*;
|
|
||||||
pub(crate) use resize_handle::*;
|
|
||||||
|
|
||||||
pub(crate) const PANEL_MIN_SIZE: Pixels = px(100.);
|
pub(crate) fn resize_handle_appearance() -> ResizeHandleRenderer {
|
||||||
|
Rc::new(
|
||||||
/// Create a [`ResizablePanelGroup`] with horizontal resizing
|
|context: &ResizeHandleContext, _: &mut Window, cx: &mut App| {
|
||||||
pub fn h_resizable(id: impl Into<ElementId>) -> ResizablePanelGroup {
|
let color = if context.is_active() {
|
||||||
ResizablePanelGroup::new(id).axis(Axis::Horizontal)
|
cx.theme().border_selected
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a [`ResizablePanelGroup`] with vertical resizing
|
|
||||||
pub fn v_resizable(id: impl Into<ElementId>) -> ResizablePanelGroup {
|
|
||||||
ResizablePanelGroup::new(id).axis(Axis::Vertical)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a [`ResizablePanel`].
|
|
||||||
pub fn resizable_panel() -> ResizablePanel {
|
|
||||||
ResizablePanel::new()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// State for a [`ResizablePanel`]
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct ResizableState {
|
|
||||||
/// The `axis` will sync to actual axis of the ResizablePanelGroup in use.
|
|
||||||
axis: Axis,
|
|
||||||
panels: Vec<ResizablePanelState>,
|
|
||||||
sizes: Vec<Pixels>,
|
|
||||||
pub(crate) resizing_panel_ix: Option<usize>,
|
|
||||||
bounds: Bounds<Pixels>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for ResizableState {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
axis: Axis::Horizontal,
|
|
||||||
panels: vec![],
|
|
||||||
sizes: vec![],
|
|
||||||
resizing_panel_ix: None,
|
|
||||||
bounds: Bounds::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ResizableState {
|
|
||||||
/// Get the size of the panels.
|
|
||||||
pub fn sizes(&self) -> &Vec<Pixels> {
|
|
||||||
&self.sizes
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn insert_panel(
|
|
||||||
&mut self,
|
|
||||||
size: Option<Pixels>,
|
|
||||||
ix: Option<usize>,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
let panel_state = ResizablePanelState {
|
|
||||||
size,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let size = size.unwrap_or(PANEL_MIN_SIZE);
|
|
||||||
|
|
||||||
// We make sure that the size always sums up to the container size
|
|
||||||
// by reducing the size of all other panels first.
|
|
||||||
let container_size = self.container_size().max(px(1.));
|
|
||||||
let total_leftover_size = (container_size - size).max(px(1.));
|
|
||||||
|
|
||||||
for (i, panel) in self.panels.iter_mut().enumerate() {
|
|
||||||
let ratio = self.sizes[i] / container_size;
|
|
||||||
self.sizes[i] = total_leftover_size * ratio;
|
|
||||||
panel.size = Some(self.sizes[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(ix) = ix {
|
|
||||||
self.panels.insert(ix, panel_state);
|
|
||||||
self.sizes.insert(ix, size);
|
|
||||||
} else {
|
} else {
|
||||||
self.panels.push(panel_state);
|
cx.theme().border
|
||||||
self.sizes.push(size);
|
|
||||||
};
|
};
|
||||||
|
let axis = context.axis();
|
||||||
|
|
||||||
cx.notify();
|
Some(
|
||||||
}
|
div()
|
||||||
|
.group_hover("handle", move |this| this.bg(color))
|
||||||
pub(crate) fn sync_panels_count(
|
.when(axis.is_horizontal(), |this| this.h_full().w(HANDLE_SIZE))
|
||||||
&mut self,
|
.when(axis.is_vertical(), |this| this.w_full().h(HANDLE_SIZE))
|
||||||
axis: Axis,
|
.into_any_element(),
|
||||||
panels_count: usize,
|
)
|
||||||
cx: &mut Context<Self>,
|
},
|
||||||
) {
|
)
|
||||||
let mut changed = self.axis != axis;
|
|
||||||
self.axis = axis;
|
|
||||||
|
|
||||||
if panels_count > self.panels.len() {
|
|
||||||
let diff = panels_count - self.panels.len();
|
|
||||||
self.panels
|
|
||||||
.extend(vec![ResizablePanelState::default(); diff]);
|
|
||||||
self.sizes.extend(vec![PANEL_MIN_SIZE; diff]);
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if panels_count < self.panels.len() {
|
|
||||||
self.panels.truncate(panels_count);
|
|
||||||
self.sizes.truncate(panels_count);
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if changed {
|
|
||||||
// We need to make sure the total size is in line with the container size.
|
|
||||||
self.adjust_to_container_size(cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn update_panel_size(
|
|
||||||
&mut self,
|
|
||||||
panel_ix: usize,
|
|
||||||
bounds: Bounds<Pixels>,
|
|
||||||
size_range: Range<Pixels>,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
let size = bounds.size.along(self.axis);
|
|
||||||
// This check is only necessary to stop the very first panel from resizing on its own
|
|
||||||
// it needs to be passed when the panel is freshly created so we get the initial size,
|
|
||||||
// but its also fine when it sometimes passes later.
|
|
||||||
if self.sizes[panel_ix].to_f64() == PANEL_MIN_SIZE.to_f64() {
|
|
||||||
self.sizes[panel_ix] = size;
|
|
||||||
self.panels[panel_ix].size = Some(size);
|
|
||||||
}
|
|
||||||
self.panels[panel_ix].bounds = bounds;
|
|
||||||
self.panels[panel_ix].size_range = size_range;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn remove_panel(&mut self, panel_ix: usize, cx: &mut Context<Self>) {
|
|
||||||
self.panels.remove(panel_ix);
|
|
||||||
self.sizes.remove(panel_ix);
|
|
||||||
if let Some(resizing_panel_ix) = self.resizing_panel_ix
|
|
||||||
&& resizing_panel_ix > panel_ix
|
|
||||||
{
|
|
||||||
self.resizing_panel_ix = Some(resizing_panel_ix - 1);
|
|
||||||
}
|
|
||||||
self.adjust_to_container_size(cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn replace_panel(
|
|
||||||
&mut self,
|
|
||||||
panel_ix: usize,
|
|
||||||
panel: ResizablePanelState,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
let old_size = self.sizes[panel_ix];
|
|
||||||
|
|
||||||
self.panels[panel_ix] = panel;
|
|
||||||
self.sizes[panel_ix] = old_size;
|
|
||||||
self.adjust_to_container_size(cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn clear(&mut self) {
|
|
||||||
self.panels.clear();
|
|
||||||
self.sizes.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn container_size(&self) -> Pixels {
|
|
||||||
self.bounds.size.along(self.axis)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn done_resizing(&mut self, cx: &mut Context<Self>) {
|
|
||||||
self.resizing_panel_ix = None;
|
|
||||||
cx.emit(ResizablePanelEvent::Resized);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn panel_size_range(&self, ix: usize) -> Range<Pixels> {
|
|
||||||
let Some(panel) = self.panels.get(ix) else {
|
|
||||||
return PANEL_MIN_SIZE..Pixels::MAX;
|
|
||||||
};
|
|
||||||
|
|
||||||
panel.size_range.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sync_real_panel_sizes(&mut self, _: &App) {
|
|
||||||
for (i, panel) in self.panels.iter().enumerate() {
|
|
||||||
self.sizes[i] = panel.bounds.size.along(self.axis);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The `ix`` is the index of the panel to resize,
|
|
||||||
/// and the `size` is the new size for the panel.
|
|
||||||
fn resize_panel(&mut self, ix: usize, size: Pixels, _: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
let old_sizes = self.sizes.clone();
|
|
||||||
|
|
||||||
let mut ix = ix;
|
|
||||||
// Only resize the left panels.
|
|
||||||
if ix >= old_sizes.len() - 1 {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let container_size = self.container_size();
|
|
||||||
self.sync_real_panel_sizes(cx);
|
|
||||||
|
|
||||||
let move_changed = size - old_sizes[ix];
|
|
||||||
if move_changed == px(0.) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let size_range = self.panel_size_range(ix);
|
|
||||||
let new_size = size.clamp(size_range.start, size_range.end);
|
|
||||||
let is_expand = move_changed > px(0.);
|
|
||||||
|
|
||||||
let main_ix = ix;
|
|
||||||
let mut new_sizes = old_sizes.clone();
|
|
||||||
|
|
||||||
if is_expand {
|
|
||||||
let mut changed = new_size - old_sizes[ix];
|
|
||||||
new_sizes[ix] = new_size;
|
|
||||||
|
|
||||||
while changed > px(0.) && ix < old_sizes.len() - 1 {
|
|
||||||
ix += 1;
|
|
||||||
let size_range = self.panel_size_range(ix);
|
|
||||||
let available_size = (new_sizes[ix] - size_range.start).max(px(0.));
|
|
||||||
let to_reduce = changed.min(available_size);
|
|
||||||
new_sizes[ix] -= to_reduce;
|
|
||||||
changed -= to_reduce;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let mut changed = new_size - size;
|
|
||||||
new_sizes[ix] = new_size;
|
|
||||||
|
|
||||||
while changed > px(0.) && ix > 0 {
|
|
||||||
ix -= 1;
|
|
||||||
let size_range = self.panel_size_range(ix);
|
|
||||||
let available_size = (new_sizes[ix] - size_range.start).max(px(0.));
|
|
||||||
let to_reduce = changed.min(available_size);
|
|
||||||
changed -= to_reduce;
|
|
||||||
new_sizes[ix] -= to_reduce;
|
|
||||||
}
|
|
||||||
|
|
||||||
new_sizes[main_ix + 1] += old_sizes[main_ix] - size - changed;
|
|
||||||
}
|
|
||||||
|
|
||||||
let total_size: Pixels = new_sizes.iter().map(|s| s.to_f64()).sum::<f64>().into();
|
|
||||||
|
|
||||||
// If total size exceeds container size, adjust the main panel
|
|
||||||
if total_size > container_size {
|
|
||||||
let overflow = total_size - container_size;
|
|
||||||
new_sizes[main_ix] = (new_sizes[main_ix] - overflow).max(size_range.start);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (i, _) in old_sizes.iter().enumerate() {
|
|
||||||
let size = new_sizes[i];
|
|
||||||
self.panels[i].size = Some(size);
|
|
||||||
}
|
|
||||||
self.sizes = new_sizes;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adjust panel sizes according to the container size.
|
|
||||||
///
|
|
||||||
/// When the container size changes, the panels should take up the same percentage as they did before.
|
|
||||||
fn adjust_to_container_size(&mut self, cx: &mut Context<Self>) {
|
|
||||||
if self.container_size().is_zero() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let container_size = self.container_size();
|
|
||||||
let total_size = px(self.sizes.iter().map(f32::from).sum::<f32>());
|
|
||||||
|
|
||||||
for i in 0..self.panels.len() {
|
|
||||||
let size = self.sizes[i];
|
|
||||||
let ratio = size / total_size;
|
|
||||||
let new_size = container_size * ratio;
|
|
||||||
|
|
||||||
self.sizes[i] = new_size;
|
|
||||||
self.panels[i].size = Some(new_size);
|
|
||||||
}
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EventEmitter<ResizablePanelEvent> for ResizableState {}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default)]
|
|
||||||
pub(crate) struct ResizablePanelState {
|
|
||||||
pub size: Option<Pixels>,
|
|
||||||
pub size_range: Range<Pixels>,
|
|
||||||
bounds: Bounds<Pixels>,
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,408 +0,0 @@
|
|||||||
use std::ops::{Deref, Range};
|
|
||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
use gpui::prelude::FluentBuilder;
|
|
||||||
use gpui::{
|
|
||||||
Along, AnyElement, App, AppContext, Axis, Bounds, Context, Element, ElementId, Empty, Entity,
|
|
||||||
EventEmitter, InteractiveElement as _, IntoElement, IsZero as _, MouseMoveEvent, MouseUpEvent,
|
|
||||||
ParentElement, Pixels, Render, RenderOnce, Style, Styled, Window, div,
|
|
||||||
};
|
|
||||||
use theme::AxisExt;
|
|
||||||
|
|
||||||
use super::{ResizableState, resizable_panel, resize_handle};
|
|
||||||
use crate::resizable::PANEL_MIN_SIZE;
|
|
||||||
use crate::{ElementExt, h_flex, v_flex};
|
|
||||||
|
|
||||||
pub enum ResizablePanelEvent {
|
|
||||||
Resized,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub(crate) struct DragPanel;
|
|
||||||
impl Render for DragPanel {
|
|
||||||
fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
|
|
||||||
Empty
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A group of resizable panels.
|
|
||||||
#[allow(clippy::type_complexity)]
|
|
||||||
#[derive(IntoElement)]
|
|
||||||
pub struct ResizablePanelGroup {
|
|
||||||
id: ElementId,
|
|
||||||
state: Option<Entity<ResizableState>>,
|
|
||||||
axis: Axis,
|
|
||||||
size: Option<Pixels>,
|
|
||||||
children: Vec<ResizablePanel>,
|
|
||||||
on_resize: Rc<dyn Fn(&Entity<ResizableState>, &mut Window, &mut App)>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ResizablePanelGroup {
|
|
||||||
/// Create a new resizable panel group.
|
|
||||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
|
||||||
Self {
|
|
||||||
id: id.into(),
|
|
||||||
axis: Axis::Horizontal,
|
|
||||||
children: vec![],
|
|
||||||
state: None,
|
|
||||||
size: None,
|
|
||||||
on_resize: Rc::new(|_, _, _| {}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Bind yourself to a resizable state entity.
|
|
||||||
///
|
|
||||||
/// If not provided, it will handle its own state internally.
|
|
||||||
pub fn with_state(mut self, state: &Entity<ResizableState>) -> Self {
|
|
||||||
self.state = Some(state.clone());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the axis of the resizable panel group, default is horizontal.
|
|
||||||
pub fn axis(mut self, axis: Axis) -> Self {
|
|
||||||
self.axis = axis;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add a panel to the group.
|
|
||||||
///
|
|
||||||
/// - The `axis` will be set to the same axis as the group.
|
|
||||||
/// - The `initial_size` will be set to the average size of all panels if not provided.
|
|
||||||
/// - The `group` will be set to the group entity.
|
|
||||||
pub fn child(mut self, panel: impl Into<ResizablePanel>) -> Self {
|
|
||||||
self.children.push(panel.into());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add multiple panels to the group.
|
|
||||||
pub fn children<I>(mut self, panels: impl IntoIterator<Item = I>) -> Self
|
|
||||||
where
|
|
||||||
I: Into<ResizablePanel>,
|
|
||||||
{
|
|
||||||
self.children = panels.into_iter().map(|panel| panel.into()).collect();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set size of the resizable panel group
|
|
||||||
///
|
|
||||||
/// - When the axis is horizontal, the size is the height of the group.
|
|
||||||
/// - When the axis is vertical, the size is the width of the group.
|
|
||||||
pub fn size(mut self, size: Pixels) -> Self {
|
|
||||||
self.size = Some(size);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the callback to be called when the panels are resized.
|
|
||||||
///
|
|
||||||
/// ## Callback arguments
|
|
||||||
///
|
|
||||||
/// - Entity<ResizableState>: The state of the ResizablePanelGroup.
|
|
||||||
pub fn on_resize(
|
|
||||||
mut self,
|
|
||||||
on_resize: impl Fn(&Entity<ResizableState>, &mut Window, &mut App) + 'static,
|
|
||||||
) -> Self {
|
|
||||||
self.on_resize = Rc::new(on_resize);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> From<T> for ResizablePanel
|
|
||||||
where
|
|
||||||
T: Into<AnyElement>,
|
|
||||||
{
|
|
||||||
fn from(value: T) -> Self {
|
|
||||||
resizable_panel().child(value.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<ResizablePanelGroup> for ResizablePanel {
|
|
||||||
fn from(value: ResizablePanelGroup) -> Self {
|
|
||||||
resizable_panel().child(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EventEmitter<ResizablePanelEvent> for ResizablePanelGroup {}
|
|
||||||
|
|
||||||
impl RenderOnce for ResizablePanelGroup {
|
|
||||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
|
||||||
let state = self.state.unwrap_or(
|
|
||||||
window.use_keyed_state(self.id.clone(), cx, |_, _| ResizableState::default()),
|
|
||||||
);
|
|
||||||
let container = if self.axis.is_horizontal() {
|
|
||||||
h_flex()
|
|
||||||
} else {
|
|
||||||
v_flex()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Sync panels to the state
|
|
||||||
let panels_count = self.children.len();
|
|
||||||
state.update(cx, |state, cx| {
|
|
||||||
state.sync_panels_count(self.axis, panels_count, cx);
|
|
||||||
});
|
|
||||||
|
|
||||||
container
|
|
||||||
.id(self.id)
|
|
||||||
.size_full()
|
|
||||||
.children(
|
|
||||||
self.children
|
|
||||||
.into_iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(ix, mut panel)| {
|
|
||||||
panel.panel_ix = ix;
|
|
||||||
panel.axis = self.axis;
|
|
||||||
panel.state = Some(state.clone());
|
|
||||||
panel
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.on_prepaint({
|
|
||||||
let state = state.clone();
|
|
||||||
move |bounds, _, cx| {
|
|
||||||
state.update(cx, |state, cx| {
|
|
||||||
let size_changed =
|
|
||||||
state.bounds.size.along(self.axis) != bounds.size.along(self.axis);
|
|
||||||
|
|
||||||
state.bounds = bounds;
|
|
||||||
|
|
||||||
if size_changed {
|
|
||||||
state.adjust_to_container_size(cx);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.child(ResizePanelGroupElement {
|
|
||||||
state: state.clone(),
|
|
||||||
axis: self.axis,
|
|
||||||
on_resize: self.on_resize.clone(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A resizable panel inside a [`ResizablePanelGroup`].
|
|
||||||
#[derive(IntoElement)]
|
|
||||||
pub struct ResizablePanel {
|
|
||||||
axis: Axis,
|
|
||||||
panel_ix: usize,
|
|
||||||
state: Option<Entity<ResizableState>>,
|
|
||||||
/// Initial size is the size that the panel has when it is created.
|
|
||||||
initial_size: Option<Pixels>,
|
|
||||||
/// size range limit of this panel.
|
|
||||||
size_range: Range<Pixels>,
|
|
||||||
children: Vec<AnyElement>,
|
|
||||||
visible: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ResizablePanel {
|
|
||||||
/// Create a new resizable panel.
|
|
||||||
pub(super) fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
panel_ix: 0,
|
|
||||||
initial_size: None,
|
|
||||||
state: None,
|
|
||||||
size_range: (PANEL_MIN_SIZE..Pixels::MAX),
|
|
||||||
axis: Axis::Horizontal,
|
|
||||||
children: vec![],
|
|
||||||
visible: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the visibility of the panel, default is true.
|
|
||||||
pub fn visible(mut self, visible: bool) -> Self {
|
|
||||||
self.visible = visible;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the initial size of the panel.
|
|
||||||
pub fn size(mut self, size: impl Into<Pixels>) -> Self {
|
|
||||||
self.initial_size = Some(size.into());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the size range to limit panel resize.
|
|
||||||
///
|
|
||||||
/// Default is [`PANEL_MIN_SIZE`] to [`Pixels::MAX`].
|
|
||||||
pub fn size_range(mut self, range: impl Into<Range<Pixels>>) -> Self {
|
|
||||||
self.size_range = range.into();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ParentElement for ResizablePanel {
|
|
||||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
|
||||||
self.children.extend(elements);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenderOnce for ResizablePanel {
|
|
||||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
|
||||||
if !self.visible {
|
|
||||||
return div().id(("resizable-panel", self.panel_ix));
|
|
||||||
}
|
|
||||||
|
|
||||||
let state = self
|
|
||||||
.state
|
|
||||||
.expect("BUG: The `state` in ResizablePanel should be present.");
|
|
||||||
|
|
||||||
let panel_state = state
|
|
||||||
.read(cx)
|
|
||||||
.panels
|
|
||||||
.get(self.panel_ix)
|
|
||||||
.expect("BUG: The `index` of ResizablePanel should be one of in `state`.");
|
|
||||||
|
|
||||||
let size_range = self.size_range.clone();
|
|
||||||
|
|
||||||
div()
|
|
||||||
.id(("resizable-panel", self.panel_ix))
|
|
||||||
.flex()
|
|
||||||
.flex_grow_1()
|
|
||||||
.size_full()
|
|
||||||
.relative()
|
|
||||||
.when(self.axis.is_vertical(), |this| {
|
|
||||||
this.min_h(size_range.start).max_h(size_range.end)
|
|
||||||
})
|
|
||||||
.when(self.axis.is_horizontal(), |this| {
|
|
||||||
this.min_w(size_range.start).max_w(size_range.end)
|
|
||||||
})
|
|
||||||
// 1. initial_size is None, to use auto size.
|
|
||||||
// 2. initial_size is Some and size is none, to use the initial size of the panel for first time render.
|
|
||||||
// 3. initial_size is Some and size is Some, use `size`.
|
|
||||||
.when(self.initial_size.is_none(), |this| this.flex_shrink_1())
|
|
||||||
.when_some(self.initial_size, |this, initial_size| {
|
|
||||||
// The `self.size` is None, that mean the initial size for the panel,
|
|
||||||
// so we need set `flex_shrink_0` To let it keep the initial size.
|
|
||||||
this.when(
|
|
||||||
panel_state.size.is_none() && !initial_size.is_zero(),
|
|
||||||
|this| this.flex_none(),
|
|
||||||
)
|
|
||||||
.flex_basis(initial_size)
|
|
||||||
})
|
|
||||||
.map(|this| match panel_state.size {
|
|
||||||
Some(size) => this.flex_basis(size.min(size_range.end).max(size_range.start)),
|
|
||||||
None => this,
|
|
||||||
})
|
|
||||||
.on_prepaint({
|
|
||||||
let state = state.clone();
|
|
||||||
move |bounds, _, cx| {
|
|
||||||
state.update(cx, |state, cx| {
|
|
||||||
state.update_panel_size(self.panel_ix, bounds, self.size_range, cx)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.children(self.children)
|
|
||||||
.when(self.panel_ix > 0, |this| {
|
|
||||||
let ix = self.panel_ix - 1;
|
|
||||||
this.child(resize_handle(("resizable-handle", ix), self.axis).on_drag(
|
|
||||||
DragPanel,
|
|
||||||
move |drag_panel, _, _, cx| {
|
|
||||||
cx.stop_propagation();
|
|
||||||
// Set current resizing panel ix
|
|
||||||
state.update(cx, |state, _| {
|
|
||||||
state.resizing_panel_ix = Some(ix);
|
|
||||||
});
|
|
||||||
cx.new(|_| drag_panel.deref().clone())
|
|
||||||
},
|
|
||||||
))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::type_complexity)]
|
|
||||||
struct ResizePanelGroupElement {
|
|
||||||
state: Entity<ResizableState>,
|
|
||||||
on_resize: Rc<dyn Fn(&Entity<ResizableState>, &mut Window, &mut App)>,
|
|
||||||
axis: Axis,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IntoElement for ResizePanelGroupElement {
|
|
||||||
type Element = Self;
|
|
||||||
|
|
||||||
fn into_element(self) -> Self::Element {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Element for ResizePanelGroupElement {
|
|
||||||
type PrepaintState = ();
|
|
||||||
type RequestLayoutState = ();
|
|
||||||
|
|
||||||
fn id(&self) -> Option<gpui::ElementId> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn request_layout(
|
|
||||||
&mut self,
|
|
||||||
_: Option<&gpui::GlobalElementId>,
|
|
||||||
_: Option<&gpui::InspectorElementId>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut App,
|
|
||||||
) -> (gpui::LayoutId, Self::RequestLayoutState) {
|
|
||||||
(window.request_layout(Style::default(), None, cx), ())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prepaint(
|
|
||||||
&mut self,
|
|
||||||
_: Option<&gpui::GlobalElementId>,
|
|
||||||
_: Option<&gpui::InspectorElementId>,
|
|
||||||
_: Bounds<Pixels>,
|
|
||||||
_: &mut Self::RequestLayoutState,
|
|
||||||
_window: &mut Window,
|
|
||||||
_cx: &mut App,
|
|
||||||
) -> Self::PrepaintState {
|
|
||||||
}
|
|
||||||
|
|
||||||
fn paint(
|
|
||||||
&mut self,
|
|
||||||
_: Option<&gpui::GlobalElementId>,
|
|
||||||
_: Option<&gpui::InspectorElementId>,
|
|
||||||
_: Bounds<Pixels>,
|
|
||||||
_: &mut Self::RequestLayoutState,
|
|
||||||
_: &mut Self::PrepaintState,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut App,
|
|
||||||
) {
|
|
||||||
window.on_mouse_event({
|
|
||||||
let state = self.state.clone();
|
|
||||||
let axis = self.axis;
|
|
||||||
let current_ix = state.read(cx).resizing_panel_ix;
|
|
||||||
move |e: &MouseMoveEvent, phase, window, cx| {
|
|
||||||
if !phase.bubble() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let Some(ix) = current_ix else { return };
|
|
||||||
|
|
||||||
state.update(cx, |state, cx| {
|
|
||||||
let panel = state.panels.get(ix).expect("BUG: invalid panel index");
|
|
||||||
|
|
||||||
match axis {
|
|
||||||
Axis::Horizontal => {
|
|
||||||
state.resize_panel(ix, e.position.x - panel.bounds.left(), window, cx)
|
|
||||||
}
|
|
||||||
Axis::Vertical => {
|
|
||||||
state.resize_panel(ix, e.position.y - panel.bounds.top(), window, cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cx.notify();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// When any mouse up, stop dragging
|
|
||||||
window.on_mouse_event({
|
|
||||||
let state = self.state.clone();
|
|
||||||
let current_ix = state.read(cx).resizing_panel_ix;
|
|
||||||
let on_resize = self.on_resize.clone();
|
|
||||||
move |_: &MouseUpEvent, phase, window, cx| {
|
|
||||||
if current_ix.is_none() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if phase.bubble() {
|
|
||||||
state.update(cx, |state, cx| state.done_resizing(cx));
|
|
||||||
on_resize(&state, window, cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,225 +0,0 @@
|
|||||||
use std::cell::Cell;
|
|
||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
use gpui::prelude::FluentBuilder as _;
|
|
||||||
use gpui::{
|
|
||||||
AnyElement, App, Axis, Element, ElementId, Entity, GlobalElementId, InteractiveElement,
|
|
||||||
IntoElement, MouseDownEvent, MouseUpEvent, ParentElement as _, Pixels, Point, Render,
|
|
||||||
StatefulInteractiveElement, Styled as _, Window, div, px,
|
|
||||||
};
|
|
||||||
use theme::{ActiveTheme, AxisExt};
|
|
||||||
|
|
||||||
use crate::dock::DockPlacement;
|
|
||||||
|
|
||||||
pub(crate) const HANDLE_PADDING: Pixels = px(4.);
|
|
||||||
pub(crate) const HANDLE_SIZE: Pixels = px(1.);
|
|
||||||
|
|
||||||
/// Create a resize handle for a resizable panel.
|
|
||||||
pub(crate) fn resize_handle<T: 'static, E: 'static + Render>(
|
|
||||||
id: impl Into<ElementId>,
|
|
||||||
axis: Axis,
|
|
||||||
) -> ResizeHandle<T, E> {
|
|
||||||
ResizeHandle::new(id, axis)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::type_complexity)]
|
|
||||||
pub(crate) struct ResizeHandle<T: 'static, E: 'static + Render> {
|
|
||||||
id: ElementId,
|
|
||||||
axis: Axis,
|
|
||||||
drag_value: Option<Rc<T>>,
|
|
||||||
placement: Option<DockPlacement>,
|
|
||||||
on_drag: Option<Rc<dyn Fn(&Point<Pixels>, &mut Window, &mut App) -> Entity<E>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: 'static, E: 'static + Render> ResizeHandle<T, E> {
|
|
||||||
fn new(id: impl Into<ElementId>, axis: Axis) -> Self {
|
|
||||||
let id = id.into();
|
|
||||||
Self {
|
|
||||||
id: id.clone(),
|
|
||||||
on_drag: None,
|
|
||||||
drag_value: None,
|
|
||||||
placement: None,
|
|
||||||
axis,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn on_drag(
|
|
||||||
mut self,
|
|
||||||
value: T,
|
|
||||||
f: impl Fn(Rc<T>, &Point<Pixels>, &mut Window, &mut App) -> Entity<E> + 'static,
|
|
||||||
) -> Self {
|
|
||||||
let value = Rc::new(value);
|
|
||||||
self.drag_value = Some(value.clone());
|
|
||||||
self.on_drag = Some(Rc::new(move |p, window, cx| {
|
|
||||||
f(value.clone(), p, window, cx)
|
|
||||||
}));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub(crate) fn placement(mut self, placement: DockPlacement) -> Self {
|
|
||||||
self.placement = Some(placement);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone)]
|
|
||||||
struct ResizeHandleState {
|
|
||||||
active: Cell<bool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ResizeHandleState {
|
|
||||||
fn set_active(&self, active: bool) {
|
|
||||||
self.active.set(active);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_active(&self) -> bool {
|
|
||||||
self.active.get()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: 'static, E: 'static + Render> IntoElement for ResizeHandle<T, E> {
|
|
||||||
type Element = ResizeHandle<T, E>;
|
|
||||||
|
|
||||||
fn into_element(self) -> Self::Element {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: 'static, E: 'static + Render> Element for ResizeHandle<T, E> {
|
|
||||||
type PrepaintState = ();
|
|
||||||
type RequestLayoutState = AnyElement;
|
|
||||||
|
|
||||||
fn id(&self) -> Option<ElementId> {
|
|
||||||
Some(self.id.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn request_layout(
|
|
||||||
&mut self,
|
|
||||||
id: Option<&GlobalElementId>,
|
|
||||||
_: Option<&gpui::InspectorElementId>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut App,
|
|
||||||
) -> (gpui::LayoutId, Self::RequestLayoutState) {
|
|
||||||
let neg_offset = -HANDLE_PADDING;
|
|
||||||
let axis = self.axis;
|
|
||||||
|
|
||||||
window.with_element_state(id.unwrap(), |state, window| {
|
|
||||||
let state = state.unwrap_or(ResizeHandleState::default());
|
|
||||||
|
|
||||||
let bg_color = if state.is_active() {
|
|
||||||
cx.theme().border_selected
|
|
||||||
} else {
|
|
||||||
cx.theme().border
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut ele = div()
|
|
||||||
.id(self.id.clone())
|
|
||||||
.occlude()
|
|
||||||
.absolute()
|
|
||||||
.flex_shrink_0()
|
|
||||||
.group("handle")
|
|
||||||
.when_some(self.on_drag.clone(), |this, on_drag| {
|
|
||||||
this.on_drag(
|
|
||||||
self.drag_value.clone().unwrap(),
|
|
||||||
move |_, position, window, cx| on_drag(&position, window, cx),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.map(|this| match self.placement {
|
|
||||||
Some(DockPlacement::Left) => {
|
|
||||||
// Special for Left Dock
|
|
||||||
// FIXME: Improve this to let the scroll bar have px(HANDLE_PADDING)
|
|
||||||
this.cursor_col_resize()
|
|
||||||
.top_0()
|
|
||||||
.right(px(1.))
|
|
||||||
.h_full()
|
|
||||||
.w(HANDLE_SIZE)
|
|
||||||
.pl(HANDLE_PADDING)
|
|
||||||
}
|
|
||||||
_ => this
|
|
||||||
.when(axis.is_horizontal(), |this| {
|
|
||||||
this.cursor_col_resize()
|
|
||||||
.top_0()
|
|
||||||
.left(neg_offset)
|
|
||||||
.h_full()
|
|
||||||
.w(HANDLE_SIZE)
|
|
||||||
.px(HANDLE_PADDING)
|
|
||||||
})
|
|
||||||
.when(axis.is_vertical(), |this| {
|
|
||||||
this.cursor_row_resize()
|
|
||||||
.top(neg_offset)
|
|
||||||
.left_0()
|
|
||||||
.w_full()
|
|
||||||
.h(HANDLE_SIZE)
|
|
||||||
.py(HANDLE_PADDING)
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.group_hover("handle", |this| this.bg(bg_color))
|
|
||||||
.when(axis.is_horizontal(), |this| this.h_full().w(HANDLE_SIZE))
|
|
||||||
.when(axis.is_vertical(), |this| this.w_full().h(HANDLE_SIZE)),
|
|
||||||
)
|
|
||||||
.into_any_element();
|
|
||||||
|
|
||||||
let layout_id = ele.request_layout(window, cx);
|
|
||||||
|
|
||||||
((layout_id, ele), state)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prepaint(
|
|
||||||
&mut self,
|
|
||||||
_: Option<&GlobalElementId>,
|
|
||||||
_: Option<&gpui::InspectorElementId>,
|
|
||||||
_: gpui::Bounds<Pixels>,
|
|
||||||
request_layout: &mut Self::RequestLayoutState,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut App,
|
|
||||||
) -> Self::PrepaintState {
|
|
||||||
request_layout.prepaint(window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn paint(
|
|
||||||
&mut self,
|
|
||||||
id: Option<&GlobalElementId>,
|
|
||||||
_: Option<&gpui::InspectorElementId>,
|
|
||||||
bounds: gpui::Bounds<Pixels>,
|
|
||||||
request_layout: &mut Self::RequestLayoutState,
|
|
||||||
_: &mut Self::PrepaintState,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut App,
|
|
||||||
) {
|
|
||||||
request_layout.paint(window, cx);
|
|
||||||
|
|
||||||
window.with_element_state(id.unwrap(), |state: Option<ResizeHandleState>, window| {
|
|
||||||
let state = state.unwrap_or_default();
|
|
||||||
|
|
||||||
window.on_mouse_event({
|
|
||||||
let state = state.clone();
|
|
||||||
move |ev: &MouseDownEvent, phase, window, _| {
|
|
||||||
if bounds.contains(&ev.position) && phase.bubble() {
|
|
||||||
state.set_active(true);
|
|
||||||
window.refresh();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
window.on_mouse_event({
|
|
||||||
let state = state.clone();
|
|
||||||
move |_: &MouseUpEvent, _, window, _| {
|
|
||||||
if state.is_active() {
|
|
||||||
state.set_active(false);
|
|
||||||
window.refresh();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
((), state)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1
-10
@@ -13,7 +13,6 @@ use theme::{
|
|||||||
CLIENT_SIDE_DECORATION_SHADOW,
|
CLIENT_SIDE_DECORATION_SHADOW,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::input::InputState;
|
|
||||||
use crate::modal::Modal;
|
use crate::modal::Modal;
|
||||||
use crate::notification::{Notification, NotificationList};
|
use crate::notification::{Notification, NotificationList};
|
||||||
|
|
||||||
@@ -50,9 +49,6 @@ pub struct Root {
|
|||||||
/// Notification layer
|
/// Notification layer
|
||||||
pub(crate) notification: Entity<NotificationList>,
|
pub(crate) notification: Entity<NotificationList>,
|
||||||
|
|
||||||
/// Current focused input
|
|
||||||
pub(crate) focused_input: Option<Entity<InputState>>,
|
|
||||||
|
|
||||||
/// App view
|
/// App view
|
||||||
view: AnyView,
|
view: AnyView,
|
||||||
}
|
}
|
||||||
@@ -60,7 +56,6 @@ pub struct Root {
|
|||||||
impl Root {
|
impl Root {
|
||||||
pub fn new(view: AnyView, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
pub fn new(view: AnyView, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
focused_input: None,
|
|
||||||
active_modals: Vec::new(),
|
active_modals: Vec::new(),
|
||||||
notification: cx.new(|cx| NotificationList::new(window, cx)),
|
notification: cx.new(|cx| NotificationList::new(window, cx)),
|
||||||
view,
|
view,
|
||||||
@@ -98,8 +93,7 @@ impl Root {
|
|||||||
Some(
|
Some(
|
||||||
div()
|
div()
|
||||||
.absolute()
|
.absolute()
|
||||||
.top_0()
|
.inset_0()
|
||||||
.right_0()
|
|
||||||
.child(root.read(cx).notification.clone()),
|
.child(root.read(cx).notification.clone()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -171,8 +165,6 @@ impl Root {
|
|||||||
|
|
||||||
/// Close the topmost modal.
|
/// Close the topmost modal.
|
||||||
pub fn close_modal(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
pub fn close_modal(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
self.focused_input = None;
|
|
||||||
|
|
||||||
if let Some(handle) = self
|
if let Some(handle) = self
|
||||||
.active_modals
|
.active_modals
|
||||||
.pop()
|
.pop()
|
||||||
@@ -187,7 +179,6 @@ impl Root {
|
|||||||
|
|
||||||
/// Close all modals.
|
/// Close all modals.
|
||||||
pub fn close_all_modals(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
pub fn close_all_modals(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
self.focused_input = None;
|
|
||||||
self.active_modals.clear();
|
self.active_modals.clear();
|
||||||
|
|
||||||
let previous_focused_handle = self
|
let previous_focused_handle = self
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
mod scrollable;
|
mod scrollable;
|
||||||
mod scrollable_mask;
|
|
||||||
mod scrollbar;
|
|
||||||
|
|
||||||
|
pub use gpui_base::{Scrollbar, ScrollbarAxis, ScrollbarHandle};
|
||||||
pub use scrollable::*;
|
pub use scrollable::*;
|
||||||
pub use scrollable_mask::*;
|
|
||||||
pub use scrollbar::*;
|
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
use std::panic::Location;
|
use std::panic::Location;
|
||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
App, Div, Element, ElementId, InteractiveElement, IntoElement, ParentElement, RenderOnce,
|
App, Div, Element, ElementId, InteractiveElement, IntoElement, ParentElement, RenderOnce,
|
||||||
ScrollHandle, Stateful, StatefulInteractiveElement, StyleRefinement, Styled, Window, div,
|
ScrollHandle, Stateful, StatefulInteractiveElement, StyleRefinement, Styled, Window, div, px,
|
||||||
};
|
};
|
||||||
|
use gpui_base::{Scrollbar, ScrollbarAxis, ScrollbarHandle};
|
||||||
|
use theme::ActiveTheme as _;
|
||||||
|
|
||||||
use super::{Scrollbar, ScrollbarAxis};
|
use crate::StyledExt as _;
|
||||||
use crate::StyledExt;
|
|
||||||
use crate::scroll::ScrollbarHandle;
|
|
||||||
|
|
||||||
/// A trait for elements that can be made scrollable with scrollbars.
|
/// A trait for elements that can be made scrollable with scrollbars.
|
||||||
pub trait ScrollableElement: InteractiveElement + Styled + ParentElement + Element {
|
pub trait ScrollableElement: InteractiveElement + Styled + ParentElement + Element {
|
||||||
@@ -23,7 +22,7 @@ pub trait ScrollableElement: InteractiveElement + Styled + ParentElement + Eleme
|
|||||||
self.child(ScrollbarLayer {
|
self.child(ScrollbarLayer {
|
||||||
id: "scrollbar_layer".into(),
|
id: "scrollbar_layer".into(),
|
||||||
axis: axis.into(),
|
axis: axis.into(),
|
||||||
scroll_handle: Rc::new(scroll_handle.clone()),
|
scroll_handle: scroll_handle.clone(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,6 +56,72 @@ pub trait ScrollableElement: InteractiveElement + Styled + ParentElement + Eleme
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The scrollbar the application's theme describes: a 10px rail holding a 6px
|
||||||
|
/// rounded thumb that grows to 8px under the pointer.
|
||||||
|
fn scrollbar<H: ScrollbarHandle + Clone>(
|
||||||
|
scroll_handle: &H,
|
||||||
|
axis: ScrollbarAxis,
|
||||||
|
cx: &App,
|
||||||
|
) -> Scrollbar {
|
||||||
|
let theme = cx.theme();
|
||||||
|
let (thumb_width, thumb_radius) = if theme.scrollbar_mode.is_scrolling() {
|
||||||
|
(px(6.), px(3.))
|
||||||
|
} else {
|
||||||
|
(px(8.), px(4.))
|
||||||
|
};
|
||||||
|
|
||||||
|
Scrollbar::new(scroll_handle).axis(axis).styles(|styles| {
|
||||||
|
styles
|
||||||
|
.track(|track| {
|
||||||
|
track
|
||||||
|
.bg(theme.scrollbar_track_background)
|
||||||
|
.border_color(theme.scrollbar_thumb_border)
|
||||||
|
.width(px(10.))
|
||||||
|
})
|
||||||
|
.track_hover(|track| track.bg(theme.scrollbar_thumb_background).width(px(10.)))
|
||||||
|
.thumb(|thumb| {
|
||||||
|
thumb
|
||||||
|
.bg(theme.scrollbar_thumb_background)
|
||||||
|
.width(thumb_width)
|
||||||
|
.inset(px(1.))
|
||||||
|
.radius(thumb_radius)
|
||||||
|
.min_length(px(48.))
|
||||||
|
})
|
||||||
|
.thumb_hover(|thumb| {
|
||||||
|
thumb
|
||||||
|
.bg(theme.scrollbar_thumb_hover_background)
|
||||||
|
.width(px(8.))
|
||||||
|
.inset(px(1.))
|
||||||
|
.radius(px(4.))
|
||||||
|
})
|
||||||
|
.thumb_active(|thumb| {
|
||||||
|
thumb
|
||||||
|
.bg(theme.scrollbar_thumb_hover_background)
|
||||||
|
.width(px(8.))
|
||||||
|
.inset(px(1.))
|
||||||
|
.radius(px(4.))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A scrollbar child that resolves the theme while rendering, which the
|
||||||
|
/// [`ScrollableElement`] helpers cannot do at the call site.
|
||||||
|
#[derive(IntoElement)]
|
||||||
|
struct ScrollbarLayer<H: ScrollbarHandle + Clone> {
|
||||||
|
id: ElementId,
|
||||||
|
axis: ScrollbarAxis,
|
||||||
|
scroll_handle: H,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<H> RenderOnce for ScrollbarLayer<H>
|
||||||
|
where
|
||||||
|
H: ScrollbarHandle + Clone + 'static,
|
||||||
|
{
|
||||||
|
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
|
scrollbar(&self.scroll_handle, self.axis, cx).id(self.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A scrollable element wrapper that adds scrollbars to an interactive element.
|
/// A scrollable element wrapper that adds scrollbars to an interactive element.
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct Scrollable<E: InteractiveElement + Styled + ParentElement + Element> {
|
pub struct Scrollable<E: InteractiveElement + Styled + ParentElement + Element> {
|
||||||
@@ -149,13 +214,7 @@ where
|
|||||||
.flex_1(),
|
.flex_1(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(render_scrollbar(
|
.child(scrollbar(&scroll_handle, self.axis, cx).id("scrollbar"))
|
||||||
"scrollbar",
|
|
||||||
&scroll_handle,
|
|
||||||
self.axis,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,45 +226,3 @@ where
|
|||||||
Self: InteractiveElement,
|
Self: InteractiveElement,
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
|
||||||
struct ScrollbarLayer<H: ScrollbarHandle + Clone> {
|
|
||||||
id: ElementId,
|
|
||||||
axis: ScrollbarAxis,
|
|
||||||
scroll_handle: Rc<H>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<H> RenderOnce for ScrollbarLayer<H>
|
|
||||||
where
|
|
||||||
H: ScrollbarHandle + Clone + 'static,
|
|
||||||
{
|
|
||||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
|
||||||
render_scrollbar(self.id, self.scroll_handle.as_ref(), self.axis, window, cx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
#[track_caller]
|
|
||||||
fn render_scrollbar<H: ScrollbarHandle + Clone>(
|
|
||||||
id: impl Into<ElementId>,
|
|
||||||
scroll_handle: &H,
|
|
||||||
axis: ScrollbarAxis,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut App,
|
|
||||||
) -> Div {
|
|
||||||
// Do not render scrollbar when inspector is picking elements,
|
|
||||||
// to allow us to pick the background elements.
|
|
||||||
let is_inspector_picking = window.is_inspector_picking(cx);
|
|
||||||
|
|
||||||
if is_inspector_picking {
|
|
||||||
return div();
|
|
||||||
}
|
|
||||||
|
|
||||||
div()
|
|
||||||
.absolute()
|
|
||||||
.top_0()
|
|
||||||
.left_0()
|
|
||||||
.right_0()
|
|
||||||
.bottom_0()
|
|
||||||
.child(Scrollbar::new(scroll_handle).id(id).axis(axis))
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,169 +0,0 @@
|
|||||||
use gpui::{
|
|
||||||
App, Axis, BorderStyle, Bounds, ContentMask, Corners, Edges, Element, ElementId, EntityId,
|
|
||||||
GlobalElementId, Hitbox, Hsla, IntoElement, IsZero as _, LayoutId, PaintQuad, Pixels, Point,
|
|
||||||
Position, ScrollHandle, ScrollWheelEvent, Size, Style, Window, px, relative,
|
|
||||||
};
|
|
||||||
use theme::AxisExt;
|
|
||||||
|
|
||||||
/// Make a scrollable mask element to cover the parent view with the mouse wheel event listening.
|
|
||||||
///
|
|
||||||
/// When the mouse wheel is scrolled, will move the `scroll_handle` scrolling with the `axis` direction.
|
|
||||||
/// You can use this `scroll_handle` to control what you want to scroll.
|
|
||||||
/// This is only can handle once axis scrolling.
|
|
||||||
pub struct ScrollableMask {
|
|
||||||
view_id: EntityId,
|
|
||||||
axis: Axis,
|
|
||||||
scroll_handle: ScrollHandle,
|
|
||||||
debug: Option<Hsla>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ScrollableMask {
|
|
||||||
/// Create a new scrollable mask element.
|
|
||||||
pub fn new(view_id: EntityId, axis: Axis, scroll_handle: &ScrollHandle) -> Self {
|
|
||||||
Self {
|
|
||||||
view_id,
|
|
||||||
scroll_handle: scroll_handle.clone(),
|
|
||||||
axis,
|
|
||||||
debug: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Enable the debug border, to show the mask bounds.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn debug(mut self) -> Self {
|
|
||||||
self.debug = Some(gpui::yellow());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IntoElement for ScrollableMask {
|
|
||||||
type Element = Self;
|
|
||||||
|
|
||||||
fn into_element(self) -> Self::Element {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Element for ScrollableMask {
|
|
||||||
type PrepaintState = Hitbox;
|
|
||||||
type RequestLayoutState = ();
|
|
||||||
|
|
||||||
fn id(&self) -> Option<ElementId> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn request_layout(
|
|
||||||
&mut self,
|
|
||||||
_: Option<&GlobalElementId>,
|
|
||||||
_: Option<&gpui::InspectorElementId>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut App,
|
|
||||||
) -> (LayoutId, Self::RequestLayoutState) {
|
|
||||||
let style = Style {
|
|
||||||
position: Position::Absolute,
|
|
||||||
flex_grow: 1.0,
|
|
||||||
flex_shrink: 1.0,
|
|
||||||
size: Size {
|
|
||||||
width: relative(1.).into(),
|
|
||||||
height: relative(1.).into(),
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
(window.request_layout(style, None, cx), ())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prepaint(
|
|
||||||
&mut self,
|
|
||||||
_: Option<&GlobalElementId>,
|
|
||||||
_: Option<&gpui::InspectorElementId>,
|
|
||||||
bounds: Bounds<Pixels>,
|
|
||||||
_: &mut Self::RequestLayoutState,
|
|
||||||
window: &mut Window,
|
|
||||||
_: &mut App,
|
|
||||||
) -> Self::PrepaintState {
|
|
||||||
// Move y to bounds height to cover the parent view.
|
|
||||||
let cover_bounds = Bounds {
|
|
||||||
origin: Point {
|
|
||||||
x: bounds.origin.x,
|
|
||||||
y: bounds.origin.y - bounds.size.height,
|
|
||||||
},
|
|
||||||
size: bounds.size,
|
|
||||||
};
|
|
||||||
|
|
||||||
window.insert_hitbox(cover_bounds, gpui::HitboxBehavior::Normal)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn paint(
|
|
||||||
&mut self,
|
|
||||||
_: Option<&GlobalElementId>,
|
|
||||||
_: Option<&gpui::InspectorElementId>,
|
|
||||||
_: Bounds<Pixels>,
|
|
||||||
_: &mut Self::RequestLayoutState,
|
|
||||||
hitbox: &mut Self::PrepaintState,
|
|
||||||
window: &mut Window,
|
|
||||||
_: &mut App,
|
|
||||||
) {
|
|
||||||
let line_height = window.line_height();
|
|
||||||
let bounds = hitbox.bounds;
|
|
||||||
|
|
||||||
window.with_content_mask(Some(ContentMask { bounds }), |window| {
|
|
||||||
if let Some(color) = self.debug {
|
|
||||||
window.paint_quad(PaintQuad {
|
|
||||||
bounds,
|
|
||||||
border_widths: Edges::all(px(1.0)),
|
|
||||||
border_color: color,
|
|
||||||
background: gpui::transparent_white().into(),
|
|
||||||
corner_radii: Corners::all(px(0.)),
|
|
||||||
border_style: BorderStyle::default(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
window.on_mouse_event({
|
|
||||||
let view_id = self.view_id;
|
|
||||||
let is_horizontal = self.axis.is_horizontal();
|
|
||||||
let scroll_handle = self.scroll_handle.clone();
|
|
||||||
let hitbox = hitbox.clone();
|
|
||||||
let mouse_position = window.mouse_position();
|
|
||||||
let last_offset = scroll_handle.offset();
|
|
||||||
|
|
||||||
move |event: &ScrollWheelEvent, phase, window, cx| {
|
|
||||||
if bounds.contains(&mouse_position)
|
|
||||||
&& phase.bubble()
|
|
||||||
&& hitbox.is_hovered(window)
|
|
||||||
{
|
|
||||||
let mut offset = scroll_handle.offset();
|
|
||||||
let mut delta = event.delta.pixel_delta(line_height);
|
|
||||||
|
|
||||||
// Limit for only one way scrolling at same time.
|
|
||||||
// When use MacBook touchpad we may get both x and y delta,
|
|
||||||
// only allows the one that more to scroll.
|
|
||||||
if !delta.x.is_zero() && !delta.y.is_zero() {
|
|
||||||
if delta.x.abs() > delta.y.abs() {
|
|
||||||
delta.y = px(0.);
|
|
||||||
} else {
|
|
||||||
delta.x = px(0.);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if is_horizontal {
|
|
||||||
offset.x += delta.x;
|
|
||||||
} else {
|
|
||||||
offset.y += delta.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
if last_offset != offset {
|
|
||||||
scroll_handle.set_offset(offset);
|
|
||||||
cx.notify(view_id);
|
|
||||||
cx.stop_propagation();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,945 +0,0 @@
|
|||||||
use std::cell::Cell;
|
|
||||||
use std::ops::Deref;
|
|
||||||
use std::panic::Location;
|
|
||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
use gpui::{
|
|
||||||
Anchor, App, Axis, BorderStyle, Bounds, ContentMask, CursorStyle, Edges, Element, ElementId,
|
|
||||||
GlobalElementId, Hitbox, HitboxBehavior, Hsla, InspectorElementId, IntoElement, IsZero,
|
|
||||||
LayoutId, ListState, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, Pixels, Point,
|
|
||||||
Position, ScrollHandle, ScrollWheelEvent, Size, Style, UniformListScrollHandle, Window, fill,
|
|
||||||
point, px, relative, size,
|
|
||||||
};
|
|
||||||
use instant::{Duration, Instant};
|
|
||||||
use theme::{ActiveTheme, AxisExt, ScrollbarMode};
|
|
||||||
|
|
||||||
/// The width of the scrollbar (THUMB_ACTIVE_INSET * 2 + THUMB_ACTIVE_WIDTH)
|
|
||||||
const WIDTH: Pixels = px(1. * 2. + 8.);
|
|
||||||
const MIN_THUMB_SIZE: f32 = 48.;
|
|
||||||
|
|
||||||
const THUMB_WIDTH: Pixels = px(6.);
|
|
||||||
const THUMB_RADIUS: Pixels = px(6. / 2.);
|
|
||||||
const THUMB_INSET: Pixels = px(1.);
|
|
||||||
|
|
||||||
const THUMB_ACTIVE_WIDTH: Pixels = px(8.);
|
|
||||||
const THUMB_ACTIVE_RADIUS: Pixels = px(8. / 2.);
|
|
||||||
const THUMB_ACTIVE_INSET: Pixels = px(1.);
|
|
||||||
|
|
||||||
const FADE_OUT_DURATION: f32 = 3.0;
|
|
||||||
const FADE_OUT_DELAY: f32 = 2.0;
|
|
||||||
|
|
||||||
/// A trait for scroll handles that can get and set offset.
|
|
||||||
pub trait ScrollbarHandle: 'static {
|
|
||||||
/// Get the current offset of the scroll handle.
|
|
||||||
fn offset(&self) -> Point<Pixels>;
|
|
||||||
/// Set the offset of the scroll handle.
|
|
||||||
fn set_offset(&self, offset: Point<Pixels>);
|
|
||||||
/// The full size of the content, including padding.
|
|
||||||
fn content_size(&self) -> Size<Pixels>;
|
|
||||||
/// Called when start dragging the scrollbar thumb.
|
|
||||||
fn start_drag(&self) {}
|
|
||||||
/// Called when end dragging the scrollbar thumb.
|
|
||||||
fn end_drag(&self) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ScrollbarHandle for ScrollHandle {
|
|
||||||
fn offset(&self) -> Point<Pixels> {
|
|
||||||
self.offset()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_offset(&self, offset: Point<Pixels>) {
|
|
||||||
self.set_offset(offset);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn content_size(&self) -> Size<Pixels> {
|
|
||||||
Size::from(self.max_offset()) + self.bounds().size
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ScrollbarHandle for UniformListScrollHandle {
|
|
||||||
fn offset(&self) -> Point<Pixels> {
|
|
||||||
self.0.borrow().base_handle.offset()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_offset(&self, offset: Point<Pixels>) {
|
|
||||||
self.0.borrow_mut().base_handle.set_offset(offset)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn content_size(&self) -> Size<Pixels> {
|
|
||||||
let base_handle = &self.0.borrow().base_handle;
|
|
||||||
Size::from(base_handle.max_offset()) + base_handle.bounds().size
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ScrollbarHandle for ListState {
|
|
||||||
fn offset(&self) -> Point<Pixels> {
|
|
||||||
self.scroll_px_offset_for_scrollbar()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_offset(&self, offset: Point<Pixels>) {
|
|
||||||
self.set_offset_from_scrollbar(offset);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn content_size(&self) -> Size<Pixels> {
|
|
||||||
Size::from(self.max_offset_for_scrollbar()) + self.viewport_bounds().size
|
|
||||||
}
|
|
||||||
|
|
||||||
fn start_drag(&self) {
|
|
||||||
self.scrollbar_drag_started();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn end_drag(&self) {
|
|
||||||
self.scrollbar_drag_ended();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[doc(hidden)]
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
struct ScrollbarState(Rc<Cell<ScrollbarStateInner>>);
|
|
||||||
|
|
||||||
#[doc(hidden)]
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
struct ScrollbarStateInner {
|
|
||||||
hovered_axis: Option<Axis>,
|
|
||||||
hovered_on_thumb: Option<Axis>,
|
|
||||||
dragged_axis: Option<Axis>,
|
|
||||||
drag_pos: Point<Pixels>,
|
|
||||||
last_scroll_offset: Point<Pixels>,
|
|
||||||
last_scroll_time: Option<Instant>,
|
|
||||||
// Last update offset
|
|
||||||
last_update: Instant,
|
|
||||||
idle_timer_scheduled: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for ScrollbarState {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self(Rc::new(Cell::new(ScrollbarStateInner {
|
|
||||||
hovered_axis: None,
|
|
||||||
hovered_on_thumb: None,
|
|
||||||
dragged_axis: None,
|
|
||||||
drag_pos: point(px(0.), px(0.)),
|
|
||||||
last_scroll_offset: point(px(0.), px(0.)),
|
|
||||||
last_scroll_time: None,
|
|
||||||
last_update: Instant::now(),
|
|
||||||
idle_timer_scheduled: false,
|
|
||||||
})))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Deref for ScrollbarState {
|
|
||||||
type Target = Rc<Cell<ScrollbarStateInner>>;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ScrollbarStateInner {
|
|
||||||
fn with_drag_pos(&self, axis: Axis, pos: Point<Pixels>) -> Self {
|
|
||||||
let mut state = *self;
|
|
||||||
if axis.is_vertical() {
|
|
||||||
state.drag_pos.y = pos.y;
|
|
||||||
} else {
|
|
||||||
state.drag_pos.x = pos.x;
|
|
||||||
}
|
|
||||||
|
|
||||||
state.dragged_axis = Some(axis);
|
|
||||||
state
|
|
||||||
}
|
|
||||||
|
|
||||||
fn with_unset_drag_pos(&self) -> Self {
|
|
||||||
let mut state = *self;
|
|
||||||
state.dragged_axis = None;
|
|
||||||
state
|
|
||||||
}
|
|
||||||
|
|
||||||
fn with_hovered(&self, axis: Option<Axis>) -> Self {
|
|
||||||
let mut state = *self;
|
|
||||||
state.hovered_axis = axis;
|
|
||||||
if axis.is_some() {
|
|
||||||
state.last_scroll_time = Some(instant::Instant::now());
|
|
||||||
}
|
|
||||||
state
|
|
||||||
}
|
|
||||||
|
|
||||||
fn with_hovered_on_thumb(&self, axis: Option<Axis>) -> Self {
|
|
||||||
let mut state = *self;
|
|
||||||
state.hovered_on_thumb = axis;
|
|
||||||
if self.is_scrollbar_visible() && axis.is_some() {
|
|
||||||
state.last_scroll_time = Some(instant::Instant::now());
|
|
||||||
}
|
|
||||||
state
|
|
||||||
}
|
|
||||||
|
|
||||||
fn with_last_scroll(
|
|
||||||
&self,
|
|
||||||
last_scroll_offset: Point<Pixels>,
|
|
||||||
last_scroll_time: Option<Instant>,
|
|
||||||
) -> Self {
|
|
||||||
let mut state = *self;
|
|
||||||
state.last_scroll_offset = last_scroll_offset;
|
|
||||||
state.last_scroll_time = last_scroll_time;
|
|
||||||
state
|
|
||||||
}
|
|
||||||
|
|
||||||
fn with_last_scroll_time(&self, t: Option<Instant>) -> Self {
|
|
||||||
let mut state = *self;
|
|
||||||
state.last_scroll_time = t;
|
|
||||||
state
|
|
||||||
}
|
|
||||||
|
|
||||||
fn with_last_update(&self, t: Instant) -> Self {
|
|
||||||
let mut state = *self;
|
|
||||||
state.last_update = t;
|
|
||||||
state
|
|
||||||
}
|
|
||||||
|
|
||||||
fn with_idle_timer_scheduled(&self, scheduled: bool) -> Self {
|
|
||||||
let mut state = *self;
|
|
||||||
state.idle_timer_scheduled = scheduled;
|
|
||||||
state
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_scrollbar_visible(&self) -> bool {
|
|
||||||
// On drag
|
|
||||||
if self.dragged_axis.is_some() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(last_time) = self.last_scroll_time {
|
|
||||||
let elapsed = Instant::now().duration_since(last_time).as_secs_f32();
|
|
||||||
elapsed < FADE_OUT_DURATION
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Scrollbar axis.
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum ScrollbarAxis {
|
|
||||||
/// Vertical scrollbar.
|
|
||||||
Vertical,
|
|
||||||
/// Horizontal scrollbar.
|
|
||||||
Horizontal,
|
|
||||||
/// Show both vertical and horizontal scrollbars.
|
|
||||||
Both,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Axis> for ScrollbarAxis {
|
|
||||||
fn from(axis: Axis) -> Self {
|
|
||||||
match axis {
|
|
||||||
Axis::Vertical => Self::Vertical,
|
|
||||||
Axis::Horizontal => Self::Horizontal,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ScrollbarAxis {
|
|
||||||
/// Return true if the scrollbar axis is vertical.
|
|
||||||
#[inline]
|
|
||||||
pub fn is_vertical(&self) -> bool {
|
|
||||||
matches!(self, Self::Vertical)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return true if the scrollbar axis is horizontal.
|
|
||||||
#[inline]
|
|
||||||
pub fn is_horizontal(&self) -> bool {
|
|
||||||
matches!(self, Self::Horizontal)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return true if the scrollbar axis is both vertical and horizontal.
|
|
||||||
#[inline]
|
|
||||||
pub fn is_both(&self) -> bool {
|
|
||||||
matches!(self, Self::Both)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return true if the scrollbar has vertical axis.
|
|
||||||
#[inline]
|
|
||||||
pub fn has_vertical(&self) -> bool {
|
|
||||||
matches!(self, Self::Vertical | Self::Both)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return true if the scrollbar has horizontal axis.
|
|
||||||
#[inline]
|
|
||||||
pub fn has_horizontal(&self) -> bool {
|
|
||||||
matches!(self, Self::Horizontal | Self::Both)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn all(&self) -> Vec<Axis> {
|
|
||||||
match self {
|
|
||||||
Self::Vertical => vec![Axis::Vertical],
|
|
||||||
Self::Horizontal => vec![Axis::Horizontal],
|
|
||||||
// This should keep Horizontal first, Vertical is the primary axis
|
|
||||||
// if Vertical not need display, then Horizontal will not keep right margin.
|
|
||||||
Self::Both => vec![Axis::Horizontal, Axis::Vertical],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Scrollbar control for scroll-area or a uniform-list.
|
|
||||||
pub struct Scrollbar {
|
|
||||||
pub(crate) id: ElementId,
|
|
||||||
axis: ScrollbarAxis,
|
|
||||||
scrollbar_mode: Option<ScrollbarMode>,
|
|
||||||
scroll_handle: Rc<dyn ScrollbarHandle>,
|
|
||||||
scroll_size: Option<Size<Pixels>>,
|
|
||||||
/// Maximum frames per second for scrolling by drag. Default is 120 FPS.
|
|
||||||
///
|
|
||||||
/// This is used to limit the update rate of the scrollbar when it is
|
|
||||||
/// being dragged for some complex interactions for reducing CPU usage.
|
|
||||||
max_fps: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Scrollbar {
|
|
||||||
/// Create a new scrollbar.
|
|
||||||
///
|
|
||||||
/// This will have both vertical and horizontal scrollbars.
|
|
||||||
#[track_caller]
|
|
||||||
pub fn new<H: ScrollbarHandle + Clone>(scroll_handle: &H) -> Self {
|
|
||||||
let caller = Location::caller();
|
|
||||||
Self {
|
|
||||||
id: ElementId::CodeLocation(*caller),
|
|
||||||
axis: ScrollbarAxis::Both,
|
|
||||||
scrollbar_mode: None,
|
|
||||||
scroll_handle: Rc::new(scroll_handle.clone()),
|
|
||||||
max_fps: 120,
|
|
||||||
scroll_size: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create with horizontal scrollbar.
|
|
||||||
#[track_caller]
|
|
||||||
pub fn horizontal<H: ScrollbarHandle + Clone>(scroll_handle: &H) -> Self {
|
|
||||||
Self::new(scroll_handle).axis(ScrollbarAxis::Horizontal)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create with vertical scrollbar.
|
|
||||||
#[track_caller]
|
|
||||||
pub fn vertical<H: ScrollbarHandle + Clone>(scroll_handle: &H) -> Self {
|
|
||||||
Self::new(scroll_handle).axis(ScrollbarAxis::Vertical)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set a specific element id, default is the [`Location::caller`].
|
|
||||||
///
|
|
||||||
/// NOTE: In most cases, you don't need to set a specific id for scrollbar.
|
|
||||||
pub fn id(mut self, id: impl Into<ElementId>) -> Self {
|
|
||||||
self.id = id.into();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the scrollbar show mode [`ScrollbarShow`], if not set use the `cx.theme().scrollbar_show`.
|
|
||||||
pub fn scrollbar_mode(mut self, mode: ScrollbarMode) -> Self {
|
|
||||||
self.scrollbar_mode = Some(mode);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set a special scroll size of the content area, default is None.
|
|
||||||
///
|
|
||||||
/// Default will sync the `content_size` from `scroll_handle`.
|
|
||||||
pub fn scroll_size(mut self, scroll_size: Size<Pixels>) -> Self {
|
|
||||||
self.scroll_size = Some(scroll_size);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set scrollbar axis.
|
|
||||||
pub fn axis(mut self, axis: impl Into<ScrollbarAxis>) -> Self {
|
|
||||||
self.axis = axis.into();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set maximum frames per second for scrolling by drag. Default is 120 FPS.
|
|
||||||
///
|
|
||||||
/// If you have very high CPU usage, consider reducing this value to improve performance.
|
|
||||||
///
|
|
||||||
/// Available values: 30..120
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub(crate) fn max_fps(mut self, max_fps: usize) -> Self {
|
|
||||||
self.max_fps = max_fps.clamp(30, 120);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the width of the scrollbar.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub(crate) const fn width() -> Pixels {
|
|
||||||
WIDTH
|
|
||||||
}
|
|
||||||
|
|
||||||
fn style_for_active(cx: &App) -> (Hsla, Hsla, Hsla, Pixels, Pixels, Pixels) {
|
|
||||||
(
|
|
||||||
cx.theme().scrollbar_thumb_hover_background,
|
|
||||||
cx.theme().scrollbar_thumb_background,
|
|
||||||
cx.theme().scrollbar_thumb_border,
|
|
||||||
THUMB_ACTIVE_WIDTH,
|
|
||||||
THUMB_ACTIVE_INSET,
|
|
||||||
THUMB_ACTIVE_RADIUS,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn style_for_hovered_thumb(cx: &App) -> (Hsla, Hsla, Hsla, Pixels, Pixels, Pixels) {
|
|
||||||
(
|
|
||||||
cx.theme().scrollbar_thumb_hover_background,
|
|
||||||
cx.theme().scrollbar_thumb_background,
|
|
||||||
cx.theme().scrollbar_thumb_border,
|
|
||||||
THUMB_ACTIVE_WIDTH,
|
|
||||||
THUMB_ACTIVE_INSET,
|
|
||||||
THUMB_ACTIVE_RADIUS,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn style_for_hovered_bar(cx: &App) -> (Hsla, Hsla, Hsla, Pixels, Pixels, Pixels) {
|
|
||||||
(
|
|
||||||
cx.theme().scrollbar_thumb_background,
|
|
||||||
cx.theme().scrollbar_thumb_border,
|
|
||||||
gpui::transparent_black(),
|
|
||||||
THUMB_ACTIVE_WIDTH,
|
|
||||||
THUMB_ACTIVE_INSET,
|
|
||||||
THUMB_ACTIVE_RADIUS,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn style_for_normal(&self, cx: &App) -> (Hsla, Hsla, Hsla, Pixels, Pixels, Pixels) {
|
|
||||||
let scrollbar_mode = self.scrollbar_mode.unwrap_or(cx.theme().scrollbar_mode);
|
|
||||||
let (width, inset, radius) = match scrollbar_mode {
|
|
||||||
ScrollbarMode::Scrolling => (THUMB_WIDTH, THUMB_INSET, THUMB_RADIUS),
|
|
||||||
_ => (THUMB_ACTIVE_WIDTH, THUMB_ACTIVE_INSET, THUMB_ACTIVE_RADIUS),
|
|
||||||
};
|
|
||||||
(
|
|
||||||
cx.theme().scrollbar_thumb_background,
|
|
||||||
cx.theme().scrollbar_track_background,
|
|
||||||
gpui::transparent_black(),
|
|
||||||
width,
|
|
||||||
inset,
|
|
||||||
radius,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn style_for_idle(&self, _cx: &App) -> (Hsla, Hsla, Hsla, Pixels, Pixels, Pixels) {
|
|
||||||
let scrollbar_mode = self.scrollbar_mode.unwrap_or(ScrollbarMode::Always);
|
|
||||||
let (width, inset, radius) = match scrollbar_mode {
|
|
||||||
ScrollbarMode::Scrolling => (THUMB_WIDTH, THUMB_INSET, THUMB_RADIUS),
|
|
||||||
_ => (THUMB_ACTIVE_WIDTH, THUMB_ACTIVE_INSET, THUMB_ACTIVE_RADIUS),
|
|
||||||
};
|
|
||||||
|
|
||||||
(
|
|
||||||
gpui::transparent_black(),
|
|
||||||
gpui::transparent_black(),
|
|
||||||
gpui::transparent_black(),
|
|
||||||
width,
|
|
||||||
inset,
|
|
||||||
radius,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IntoElement for Scrollbar {
|
|
||||||
type Element = Self;
|
|
||||||
|
|
||||||
fn into_element(self) -> Self::Element {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[doc(hidden)]
|
|
||||||
pub struct PrepaintState {
|
|
||||||
hitbox: Hitbox,
|
|
||||||
scrollbar_state: ScrollbarState,
|
|
||||||
states: Vec<AxisPrepaintState>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[doc(hidden)]
|
|
||||||
pub struct AxisPrepaintState {
|
|
||||||
axis: Axis,
|
|
||||||
bar_hitbox: Hitbox,
|
|
||||||
bounds: Bounds<Pixels>,
|
|
||||||
radius: Pixels,
|
|
||||||
bg: Hsla,
|
|
||||||
border: Hsla,
|
|
||||||
thumb_bounds: Bounds<Pixels>,
|
|
||||||
// Bounds of thumb to be rendered.
|
|
||||||
thumb_fill_bounds: Bounds<Pixels>,
|
|
||||||
thumb_bg: Hsla,
|
|
||||||
scroll_size: Pixels,
|
|
||||||
container_size: Pixels,
|
|
||||||
thumb_size: Pixels,
|
|
||||||
margin_end: Pixels,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Element for Scrollbar {
|
|
||||||
type PrepaintState = PrepaintState;
|
|
||||||
type RequestLayoutState = ();
|
|
||||||
|
|
||||||
fn id(&self) -> Option<gpui::ElementId> {
|
|
||||||
Some(self.id.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn request_layout(
|
|
||||||
&mut self,
|
|
||||||
_: Option<&GlobalElementId>,
|
|
||||||
_: Option<&InspectorElementId>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut App,
|
|
||||||
) -> (LayoutId, Self::RequestLayoutState) {
|
|
||||||
let style = Style {
|
|
||||||
position: Position::Absolute,
|
|
||||||
flex_grow: 1.0,
|
|
||||||
flex_shrink: 1.0,
|
|
||||||
size: Size {
|
|
||||||
width: relative(1.).into(),
|
|
||||||
height: relative(1.).into(),
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
(window.request_layout(style, None, cx), ())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prepaint(
|
|
||||||
&mut self,
|
|
||||||
_: Option<&GlobalElementId>,
|
|
||||||
_: Option<&InspectorElementId>,
|
|
||||||
bounds: Bounds<Pixels>,
|
|
||||||
_: &mut Self::RequestLayoutState,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut App,
|
|
||||||
) -> Self::PrepaintState {
|
|
||||||
let hitbox = window.with_content_mask(Some(ContentMask { bounds }), |window| {
|
|
||||||
window.insert_hitbox(bounds, HitboxBehavior::Normal)
|
|
||||||
});
|
|
||||||
|
|
||||||
let state = window
|
|
||||||
.use_state(cx, |_, _| ScrollbarState::default())
|
|
||||||
.read(cx)
|
|
||||||
.clone();
|
|
||||||
|
|
||||||
let mut states = vec![];
|
|
||||||
let mut has_both = self.axis.is_both();
|
|
||||||
|
|
||||||
let scroll_size = self
|
|
||||||
.scroll_size
|
|
||||||
.unwrap_or(self.scroll_handle.content_size());
|
|
||||||
|
|
||||||
for axis in self.axis.all().into_iter() {
|
|
||||||
let is_vertical = axis.is_vertical();
|
|
||||||
let (scroll_area_size, container_size, scroll_position) = if is_vertical {
|
|
||||||
(
|
|
||||||
scroll_size.height,
|
|
||||||
hitbox.size.height,
|
|
||||||
self.scroll_handle.offset().y,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
(
|
|
||||||
scroll_size.width,
|
|
||||||
hitbox.size.width,
|
|
||||||
self.scroll_handle.offset().x,
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
// The horizontal scrollbar is set avoid overlapping with the vertical scrollbar, if the vertical scrollbar is visible.
|
|
||||||
let margin_end = if has_both && !is_vertical {
|
|
||||||
WIDTH
|
|
||||||
} else {
|
|
||||||
px(0.)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Hide scrollbar, if the scroll area is smaller than the container.
|
|
||||||
if scroll_area_size <= container_size {
|
|
||||||
has_both = false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let thumb_length =
|
|
||||||
(container_size / scroll_area_size * container_size).max(px(MIN_THUMB_SIZE));
|
|
||||||
let thumb_start = -(scroll_position / (scroll_area_size - container_size)
|
|
||||||
* (container_size - margin_end - thumb_length));
|
|
||||||
let thumb_end = (thumb_start + thumb_length).min(container_size - margin_end);
|
|
||||||
|
|
||||||
let bounds = Bounds {
|
|
||||||
origin: if is_vertical {
|
|
||||||
point(hitbox.origin.x + hitbox.size.width - WIDTH, hitbox.origin.y)
|
|
||||||
} else {
|
|
||||||
point(
|
|
||||||
hitbox.origin.x,
|
|
||||||
hitbox.origin.y + hitbox.size.height - WIDTH,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
size: gpui::Size {
|
|
||||||
width: if is_vertical {
|
|
||||||
WIDTH
|
|
||||||
} else {
|
|
||||||
hitbox.size.width
|
|
||||||
},
|
|
||||||
height: if is_vertical {
|
|
||||||
hitbox.size.height
|
|
||||||
} else {
|
|
||||||
WIDTH
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let scrollbar_show = self.scrollbar_mode.unwrap_or(cx.theme().scrollbar_mode);
|
|
||||||
let is_always_to_show = scrollbar_show.is_always();
|
|
||||||
let is_hover_to_show = scrollbar_show.is_hover();
|
|
||||||
let is_hovered_on_bar = state.get().hovered_axis == Some(axis);
|
|
||||||
let is_hovered_on_thumb = state.get().hovered_on_thumb == Some(axis);
|
|
||||||
let is_offset_changed = state.get().last_scroll_offset != self.scroll_handle.offset();
|
|
||||||
|
|
||||||
let (thumb_bg, bar_bg, bar_border, thumb_width, inset, radius) =
|
|
||||||
if state.get().dragged_axis == Some(axis) {
|
|
||||||
Self::style_for_active(cx)
|
|
||||||
} else if is_hover_to_show && (is_hovered_on_bar || is_hovered_on_thumb) {
|
|
||||||
if is_hovered_on_thumb {
|
|
||||||
Self::style_for_hovered_thumb(cx)
|
|
||||||
} else {
|
|
||||||
Self::style_for_hovered_bar(cx)
|
|
||||||
}
|
|
||||||
} else if is_offset_changed {
|
|
||||||
self.style_for_normal(cx)
|
|
||||||
} else if is_always_to_show {
|
|
||||||
if is_hovered_on_thumb {
|
|
||||||
Self::style_for_hovered_thumb(cx)
|
|
||||||
} else {
|
|
||||||
Self::style_for_hovered_bar(cx)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let mut idle_state = self.style_for_idle(cx);
|
|
||||||
// Delay 2s to fade out the scrollbar thumb (in 1s)
|
|
||||||
if let Some(last_time) = state.get().last_scroll_time {
|
|
||||||
let elapsed = Instant::now().duration_since(last_time).as_secs_f32();
|
|
||||||
if is_hovered_on_bar {
|
|
||||||
state.set(state.get().with_last_scroll_time(Some(Instant::now())));
|
|
||||||
idle_state = if is_hovered_on_thumb {
|
|
||||||
Self::style_for_hovered_thumb(cx)
|
|
||||||
} else {
|
|
||||||
Self::style_for_hovered_bar(cx)
|
|
||||||
};
|
|
||||||
} else if elapsed < FADE_OUT_DELAY {
|
|
||||||
idle_state.0 = cx.theme().scrollbar_thumb_background;
|
|
||||||
|
|
||||||
if !state.get().idle_timer_scheduled {
|
|
||||||
let state = state.clone();
|
|
||||||
state.set(state.get().with_idle_timer_scheduled(true));
|
|
||||||
let current_view = window.current_view();
|
|
||||||
let next_delay = Duration::from_secs_f32(FADE_OUT_DELAY - elapsed);
|
|
||||||
window
|
|
||||||
.spawn(cx, async move |cx| {
|
|
||||||
cx.background_executor().timer(next_delay).await;
|
|
||||||
state.set(state.get().with_idle_timer_scheduled(false));
|
|
||||||
cx.update(|_, cx| cx.notify(current_view)).ok();
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
}
|
|
||||||
} else if elapsed < FADE_OUT_DURATION {
|
|
||||||
let opacity = 1.0 - (elapsed - FADE_OUT_DELAY).powi(10);
|
|
||||||
idle_state.0 = cx.theme().scrollbar_thumb_background.opacity(opacity);
|
|
||||||
|
|
||||||
window.request_animation_frame();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
idle_state
|
|
||||||
};
|
|
||||||
|
|
||||||
// The clickable area of the thumb
|
|
||||||
let thumb_length = thumb_end - thumb_start - inset * 2;
|
|
||||||
let thumb_bounds = if is_vertical {
|
|
||||||
Bounds::from_anchor_and_size(
|
|
||||||
Anchor::TopRight,
|
|
||||||
bounds.top_right() + point(-inset, inset + thumb_start),
|
|
||||||
size(WIDTH, thumb_length),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Bounds::from_anchor_and_size(
|
|
||||||
Anchor::BottomLeft,
|
|
||||||
bounds.bottom_left() + point(inset + thumb_start, -inset),
|
|
||||||
size(thumb_length, WIDTH),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
// The actual render area of the thumb
|
|
||||||
let thumb_fill_bounds = if is_vertical {
|
|
||||||
Bounds::from_anchor_and_size(
|
|
||||||
Anchor::TopRight,
|
|
||||||
bounds.top_right() + point(-inset, inset + thumb_start),
|
|
||||||
size(thumb_width, thumb_length),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Bounds::from_anchor_and_size(
|
|
||||||
Anchor::BottomLeft,
|
|
||||||
bounds.bottom_left() + point(inset + thumb_start, -inset),
|
|
||||||
size(thumb_length, thumb_width),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
let bar_hitbox = window.with_content_mask(Some(ContentMask { bounds }), |window| {
|
|
||||||
window.insert_hitbox(bounds, gpui::HitboxBehavior::Normal)
|
|
||||||
});
|
|
||||||
|
|
||||||
states.push(AxisPrepaintState {
|
|
||||||
axis,
|
|
||||||
bar_hitbox,
|
|
||||||
bounds,
|
|
||||||
radius,
|
|
||||||
bg: bar_bg,
|
|
||||||
border: bar_border,
|
|
||||||
thumb_bounds,
|
|
||||||
thumb_fill_bounds,
|
|
||||||
thumb_bg,
|
|
||||||
scroll_size: scroll_area_size,
|
|
||||||
container_size,
|
|
||||||
thumb_size: thumb_length,
|
|
||||||
margin_end,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
PrepaintState {
|
|
||||||
hitbox,
|
|
||||||
states,
|
|
||||||
scrollbar_state: state,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn paint(
|
|
||||||
&mut self,
|
|
||||||
_: Option<&GlobalElementId>,
|
|
||||||
_: Option<&InspectorElementId>,
|
|
||||||
_: Bounds<Pixels>,
|
|
||||||
_: &mut Self::RequestLayoutState,
|
|
||||||
prepaint: &mut Self::PrepaintState,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut App,
|
|
||||||
) {
|
|
||||||
let scrollbar_state = &prepaint.scrollbar_state;
|
|
||||||
let scrollbar_show = self.scrollbar_mode.unwrap_or(cx.theme().scrollbar_mode);
|
|
||||||
let view_id = window.current_view();
|
|
||||||
let hitbox_bounds = prepaint.hitbox.bounds;
|
|
||||||
let is_visible = scrollbar_state.get().is_scrollbar_visible() || scrollbar_show.is_always();
|
|
||||||
let is_hover_to_show = scrollbar_show.is_hover();
|
|
||||||
|
|
||||||
// Update last_scroll_time when offset is changed.
|
|
||||||
if self.scroll_handle.offset() != scrollbar_state.get().last_scroll_offset {
|
|
||||||
scrollbar_state.set(
|
|
||||||
scrollbar_state
|
|
||||||
.get()
|
|
||||||
.with_last_scroll(self.scroll_handle.offset(), Some(Instant::now())),
|
|
||||||
);
|
|
||||||
cx.notify(view_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
window.with_content_mask(
|
|
||||||
Some(ContentMask {
|
|
||||||
bounds: hitbox_bounds,
|
|
||||||
}),
|
|
||||||
|window| {
|
|
||||||
for state in prepaint.states.iter() {
|
|
||||||
let axis = state.axis;
|
|
||||||
let mut radius = state.radius;
|
|
||||||
if cx.theme().radius.is_zero() {
|
|
||||||
radius = px(0.);
|
|
||||||
}
|
|
||||||
let bounds = state.bounds;
|
|
||||||
let thumb_bounds = state.thumb_bounds;
|
|
||||||
let scroll_area_size = state.scroll_size;
|
|
||||||
let container_size = state.container_size;
|
|
||||||
let thumb_size = state.thumb_size;
|
|
||||||
let margin_end = state.margin_end;
|
|
||||||
let is_vertical = axis.is_vertical();
|
|
||||||
|
|
||||||
window.set_cursor_style(CursorStyle::default(), &state.bar_hitbox);
|
|
||||||
|
|
||||||
window.paint_layer(hitbox_bounds, |cx| {
|
|
||||||
cx.paint_quad(fill(state.bounds, state.bg));
|
|
||||||
|
|
||||||
cx.paint_quad(PaintQuad {
|
|
||||||
bounds,
|
|
||||||
corner_radii: (0.).into(),
|
|
||||||
background: gpui::transparent_black().into(),
|
|
||||||
border_widths: Edges {
|
|
||||||
top: px(0.),
|
|
||||||
right: px(0.),
|
|
||||||
bottom: px(0.),
|
|
||||||
left: px(0.),
|
|
||||||
},
|
|
||||||
border_color: state.border,
|
|
||||||
border_style: BorderStyle::default(),
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.paint_quad(
|
|
||||||
fill(state.thumb_fill_bounds, state.thumb_bg).corner_radii(radius),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
window.on_mouse_event({
|
|
||||||
let state = scrollbar_state.clone();
|
|
||||||
let scroll_handle = self.scroll_handle.clone();
|
|
||||||
|
|
||||||
move |event: &ScrollWheelEvent, phase, _, cx| {
|
|
||||||
if phase.bubble()
|
|
||||||
&& hitbox_bounds.contains(&event.position)
|
|
||||||
&& scroll_handle.offset() != state.get().last_scroll_offset
|
|
||||||
{
|
|
||||||
state.set(state.get().with_last_scroll(
|
|
||||||
scroll_handle.offset(),
|
|
||||||
Some(Instant::now()),
|
|
||||||
));
|
|
||||||
cx.notify(view_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let safe_range = (-scroll_area_size + container_size)..px(0.);
|
|
||||||
|
|
||||||
if is_hover_to_show || is_visible {
|
|
||||||
window.on_mouse_event({
|
|
||||||
let state = scrollbar_state.clone();
|
|
||||||
let scroll_handle = self.scroll_handle.clone();
|
|
||||||
|
|
||||||
move |event: &MouseDownEvent, phase, _, cx| {
|
|
||||||
if phase.bubble() && bounds.contains(&event.position) {
|
|
||||||
cx.stop_propagation();
|
|
||||||
|
|
||||||
if thumb_bounds.contains(&event.position) {
|
|
||||||
// click on the thumb bar, set the drag position
|
|
||||||
let pos = event.position - thumb_bounds.origin;
|
|
||||||
|
|
||||||
scroll_handle.start_drag();
|
|
||||||
state.set(state.get().with_drag_pos(axis, pos));
|
|
||||||
|
|
||||||
cx.notify(view_id);
|
|
||||||
} else {
|
|
||||||
// click on the scrollbar, jump to the position
|
|
||||||
// Set the thumb bar center to the click position
|
|
||||||
let offset = scroll_handle.offset();
|
|
||||||
let percentage = if is_vertical {
|
|
||||||
(event.position.y - thumb_size / 2. - bounds.origin.y)
|
|
||||||
/ (bounds.size.height - thumb_size)
|
|
||||||
} else {
|
|
||||||
(event.position.x - thumb_size / 2. - bounds.origin.x)
|
|
||||||
/ (bounds.size.width - thumb_size)
|
|
||||||
}
|
|
||||||
.min(1.);
|
|
||||||
|
|
||||||
if is_vertical {
|
|
||||||
scroll_handle.set_offset(point(
|
|
||||||
offset.x,
|
|
||||||
(-scroll_area_size * percentage)
|
|
||||||
.clamp(safe_range.start, safe_range.end),
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
scroll_handle.set_offset(point(
|
|
||||||
(-scroll_area_size * percentage)
|
|
||||||
.clamp(safe_range.start, safe_range.end),
|
|
||||||
offset.y,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
window.on_mouse_event({
|
|
||||||
let scroll_handle = self.scroll_handle.clone();
|
|
||||||
let state = scrollbar_state.clone();
|
|
||||||
let max_fps_duration = Duration::from_millis((1000 / self.max_fps) as u64);
|
|
||||||
|
|
||||||
move |event: &MouseMoveEvent, _, _, cx| {
|
|
||||||
let mut notify = false;
|
|
||||||
// When is hover to show mode or it was visible,
|
|
||||||
// we need to update the hovered state and increase the last_scroll_time.
|
|
||||||
let need_hover_to_update = is_hover_to_show || is_visible;
|
|
||||||
// Update hovered state for scrollbar
|
|
||||||
if bounds.contains(&event.position) && need_hover_to_update {
|
|
||||||
state.set(state.get().with_hovered(Some(axis)));
|
|
||||||
|
|
||||||
if state.get().hovered_axis != Some(axis) {
|
|
||||||
notify = true;
|
|
||||||
}
|
|
||||||
} else if state.get().hovered_axis == Some(axis) {
|
|
||||||
state.set(state.get().with_hovered(None));
|
|
||||||
notify = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update hovered state for scrollbar thumb
|
|
||||||
if thumb_bounds.contains(&event.position) {
|
|
||||||
if state.get().hovered_on_thumb != Some(axis) {
|
|
||||||
state.set(state.get().with_hovered_on_thumb(Some(axis)));
|
|
||||||
notify = true;
|
|
||||||
}
|
|
||||||
} else if state.get().hovered_on_thumb == Some(axis) {
|
|
||||||
state.set(state.get().with_hovered_on_thumb(None));
|
|
||||||
notify = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Move thumb position on dragging
|
|
||||||
if state.get().dragged_axis == Some(axis) && event.dragging() {
|
|
||||||
// Stop the event propagation to avoid selecting text or other side effects.
|
|
||||||
cx.stop_propagation();
|
|
||||||
|
|
||||||
// drag_pos is the position of the mouse down event
|
|
||||||
// We need to keep the thumb bar still at the origin down position
|
|
||||||
let drag_pos = state.get().drag_pos;
|
|
||||||
|
|
||||||
let percentage = (if is_vertical {
|
|
||||||
(event.position.y - drag_pos.y - bounds.origin.y)
|
|
||||||
/ (bounds.size.height - thumb_size)
|
|
||||||
} else {
|
|
||||||
(event.position.x - drag_pos.x - bounds.origin.x)
|
|
||||||
/ (bounds.size.width - thumb_size - margin_end)
|
|
||||||
})
|
|
||||||
.clamp(0., 1.);
|
|
||||||
|
|
||||||
let offset = if is_vertical {
|
|
||||||
point(
|
|
||||||
scroll_handle.offset().x,
|
|
||||||
(-(scroll_area_size - container_size) * percentage)
|
|
||||||
.clamp(safe_range.start, safe_range.end),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
point(
|
|
||||||
(-(scroll_area_size - container_size) * percentage)
|
|
||||||
.clamp(safe_range.start, safe_range.end),
|
|
||||||
scroll_handle.offset().y,
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
if (scroll_handle.offset().y - offset.y).abs() > px(1.)
|
|
||||||
|| (scroll_handle.offset().x - offset.x).abs() > px(1.)
|
|
||||||
{
|
|
||||||
// Limit update rate
|
|
||||||
if state.get().last_update.elapsed() > max_fps_duration {
|
|
||||||
scroll_handle.set_offset(offset);
|
|
||||||
state.set(state.get().with_last_update(Instant::now()));
|
|
||||||
notify = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if notify {
|
|
||||||
cx.notify(view_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
window.on_mouse_event({
|
|
||||||
let state = scrollbar_state.clone();
|
|
||||||
let scroll_handle = self.scroll_handle.clone();
|
|
||||||
|
|
||||||
move |_event: &MouseUpEvent, phase, _, cx| {
|
|
||||||
if phase.bubble() {
|
|
||||||
scroll_handle.end_drag();
|
|
||||||
state.set(state.get().with_unset_drag_pos());
|
|
||||||
cx.notify(view_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1
-26
@@ -1,4 +1,5 @@
|
|||||||
use gpui::{App, DefiniteLength, Div, Edges, Pixels, Refineable, StyleRefinement, Styled, div, px};
|
use gpui::{App, DefiniteLength, Div, Edges, Pixels, Refineable, StyleRefinement, Styled, div, px};
|
||||||
|
pub use gpui_base::component_traits::{Collapsible, Disableable, Selectable};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
|
|
||||||
@@ -110,26 +111,6 @@ impl From<Pixels> for Size {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A trait for defining element that can be selected.
|
|
||||||
pub trait Selectable: Sized {
|
|
||||||
/// Set the selected state of the element.
|
|
||||||
fn selected(self, selected: bool) -> Self;
|
|
||||||
|
|
||||||
/// Returns true if the element is selected.
|
|
||||||
fn is_selected(&self) -> bool;
|
|
||||||
|
|
||||||
/// Set is the element mouse right clicked, default do nothing.
|
|
||||||
fn secondary_selected(self, _: bool) -> Self {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A trait for defining element that can be disabled.
|
|
||||||
pub trait Disableable {
|
|
||||||
/// Set the disabled state of the element.
|
|
||||||
fn disabled(self, disabled: bool) -> Self;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A trait for setting the size of an element.
|
/// A trait for setting the size of an element.
|
||||||
pub trait Sizable: Sized {
|
pub trait Sizable: Sized {
|
||||||
/// Set the ui::Size of this element.
|
/// Set the ui::Size of this element.
|
||||||
@@ -267,9 +248,3 @@ impl<T: Styled> StyleSized<T> for T {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A trait for defining element that can be collapsed.
|
|
||||||
pub trait Collapsible {
|
|
||||||
fn collapsed(self, collapsed: bool) -> Self;
|
|
||||||
fn is_collapsed(&self) -> bool;
|
|
||||||
}
|
|
||||||
|
|||||||
+44
-143
@@ -1,19 +1,19 @@
|
|||||||
use std::cell::RefCell;
|
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use instant::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use gpui::prelude::FluentBuilder as _;
|
use gpui::prelude::FluentBuilder as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Animation, AnimationExt as _, AnyElement, App, Element, ElementId, GlobalElementId,
|
App, ElementId, IntoElement, ParentElement as _, RenderOnce, SharedString, Styled as _, Window,
|
||||||
InteractiveElement, IntoElement, LayoutId, ParentElement as _, SharedString, Styled as _,
|
div, px, white,
|
||||||
Window, div, px, white,
|
|
||||||
};
|
};
|
||||||
|
use gpui_base::{Spring, Switch as BaseSwitch, SwitchThumb, SwitchTrack, spring};
|
||||||
use theme::{ActiveTheme, Side};
|
use theme::{ActiveTheme, Side};
|
||||||
|
|
||||||
use crate::{Disableable, Sizable, Size};
|
use crate::{Disableable, Sizable, Size};
|
||||||
|
|
||||||
type OnClick = Option<Rc<dyn Fn(&bool, &mut Window, &mut App)>>;
|
type OnClick = Option<Rc<dyn Fn(&bool, &mut Window, &mut App)>>;
|
||||||
|
|
||||||
|
#[derive(IntoElement)]
|
||||||
pub struct Switch {
|
pub struct Switch {
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
checked: bool,
|
checked: bool,
|
||||||
@@ -84,48 +84,14 @@ impl Disableable for Switch {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoElement for Switch {
|
impl RenderOnce for Switch {
|
||||||
type Element = Self;
|
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
|
|
||||||
fn into_element(self) -> Self::Element {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct SwitchState {
|
|
||||||
prev_checked: Rc<RefCell<Option<bool>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Element for Switch {
|
|
||||||
type PrepaintState = ();
|
|
||||||
type RequestLayoutState = AnyElement;
|
|
||||||
|
|
||||||
fn id(&self) -> Option<ElementId> {
|
|
||||||
Some(self.id.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn request_layout(
|
|
||||||
&mut self,
|
|
||||||
global_id: Option<&GlobalElementId>,
|
|
||||||
_: Option<&gpui::InspectorElementId>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut App,
|
|
||||||
) -> (LayoutId, Self::RequestLayoutState) {
|
|
||||||
window.with_element_state::<SwitchState, _>(global_id.unwrap(), move |state, window| {
|
|
||||||
let state = state.unwrap_or_default();
|
|
||||||
|
|
||||||
let theme = cx.theme();
|
|
||||||
let checked = self.checked;
|
let checked = self.checked;
|
||||||
let on_click = self.on_click.clone();
|
let on_click = self.on_click.clone();
|
||||||
|
|
||||||
let (bg, toggle_bg) = match self.checked {
|
let (bg, toggle_bg) = match checked {
|
||||||
true => (theme.element_background, white()),
|
true => (cx.theme().element_background, white()),
|
||||||
false => (theme.elevated_surface_background, white()),
|
false => (cx.theme().elevated_surface_background, white()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let (bg, toggle_bg) = match self.disabled {
|
let (bg, toggle_bg) = match self.disabled {
|
||||||
@@ -145,10 +111,31 @@ impl Element for Switch {
|
|||||||
|
|
||||||
let inset = px(2.);
|
let inset = px(2.);
|
||||||
|
|
||||||
let mut element = div()
|
let thumb_left = spring(
|
||||||
.child(
|
(self.id.clone(), "thumb"),
|
||||||
div()
|
if checked {
|
||||||
.id(self.id.clone())
|
bg_width - bar_width - inset * 2.
|
||||||
|
} else {
|
||||||
|
px(0.)
|
||||||
|
},
|
||||||
|
Spring::new(Duration::from_secs_f64(0.15)),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
|
let accessibility_label = self.label.clone();
|
||||||
|
let label = self.label;
|
||||||
|
|
||||||
|
div().child(
|
||||||
|
BaseSwitch::new(self.id.clone())
|
||||||
|
.checked(checked)
|
||||||
|
.disabled(self.disabled)
|
||||||
|
.when_some(accessibility_label, |this, label| {
|
||||||
|
this.accessibility_label(label)
|
||||||
|
})
|
||||||
|
.when_some(on_click, |this, on_click| {
|
||||||
|
this.on_change(move |next, _event, window, cx| on_click(&next, window, cx))
|
||||||
|
})
|
||||||
.when(self.label_side.is_left(), |this| this.flex_row_reverse())
|
.when(self.label_side.is_left(), |this| this.flex_row_reverse())
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
@@ -157,16 +144,15 @@ impl Element for Switch {
|
|||||||
.justify_between()
|
.justify_between()
|
||||||
.items_center()
|
.items_center()
|
||||||
.gap_4()
|
.gap_4()
|
||||||
.when_some(self.label.clone(), |this, label| {
|
.when_some(label, |this, label| {
|
||||||
// Label
|
// Label
|
||||||
this.child(
|
this.child(div().text_sm().text_color(cx.theme().text).child(label))
|
||||||
div().text_sm().text_color(cx.theme().text).child(label),
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
.child(
|
.child(
|
||||||
// Switch Bar
|
// Switch Bar
|
||||||
div()
|
SwitchTrack::new((self.id.clone(), "track"))
|
||||||
.id(self.id.clone())
|
.checked(checked)
|
||||||
|
.disabled(self.disabled)
|
||||||
.flex_shrink_0()
|
.flex_shrink_0()
|
||||||
.w(bg_width)
|
.w(bg_width)
|
||||||
.h(bg_height)
|
.h(bg_height)
|
||||||
@@ -174,59 +160,17 @@ impl Element for Switch {
|
|||||||
.flex()
|
.flex()
|
||||||
.items_center()
|
.items_center()
|
||||||
.border(inset)
|
.border(inset)
|
||||||
.border_color(theme.border_transparent)
|
.border_color(cx.theme().border_transparent)
|
||||||
.bg(bg)
|
.bg(bg)
|
||||||
.when(!self.disabled, |this| this.cursor_pointer())
|
.when(!self.disabled, |this| this.cursor_pointer())
|
||||||
.child(
|
.child(
|
||||||
// Switch Toggle
|
// Switch Toggle
|
||||||
div()
|
SwitchThumb::new(checked)
|
||||||
.rounded_full()
|
.rounded_full()
|
||||||
.when(cx.theme().shadow, |this| this.shadow_sm())
|
.when(cx.theme().shadow, |this| this.shadow_sm())
|
||||||
.bg(toggle_bg)
|
.bg(toggle_bg)
|
||||||
.size(bar_width)
|
.size(bar_width)
|
||||||
.map(|this| {
|
.left(thumb_left),
|
||||||
let prev_checked = state.prev_checked.clone();
|
|
||||||
if !self.disabled
|
|
||||||
&& prev_checked
|
|
||||||
.borrow()
|
|
||||||
.is_some_and(|prev| prev != checked)
|
|
||||||
{
|
|
||||||
let dur = Duration::from_secs_f64(0.15);
|
|
||||||
cx.spawn(async move |cx| {
|
|
||||||
cx.background_executor()
|
|
||||||
.timer(dur)
|
|
||||||
.await;
|
|
||||||
*prev_checked.borrow_mut() =
|
|
||||||
Some(checked);
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
this.with_animation(
|
|
||||||
ElementId::NamedInteger(
|
|
||||||
"move".into(),
|
|
||||||
checked as u64,
|
|
||||||
),
|
|
||||||
Animation::new(dur),
|
|
||||||
move |this, delta| {
|
|
||||||
let max_x = bg_width
|
|
||||||
- bar_width
|
|
||||||
- inset * 2;
|
|
||||||
let x = if checked {
|
|
||||||
max_x * delta
|
|
||||||
} else {
|
|
||||||
max_x - max_x * delta
|
|
||||||
};
|
|
||||||
this.left(x)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.into_any_element()
|
|
||||||
} else {
|
|
||||||
let max_x =
|
|
||||||
bg_width - bar_width - inset * 2;
|
|
||||||
let x =
|
|
||||||
if checked { max_x } else { px(0.) };
|
|
||||||
this.left(x).into_any_element()
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -238,50 +182,7 @@ impl Element for Switch {
|
|||||||
.text_color(cx.theme().text_muted)
|
.text_color(cx.theme().text_muted)
|
||||||
.child(description),
|
.child(description),
|
||||||
)
|
)
|
||||||
})
|
}),
|
||||||
.when_some(
|
|
||||||
on_click
|
|
||||||
.as_ref()
|
|
||||||
.map(|c| c.clone())
|
|
||||||
.filter(|_| !self.disabled),
|
|
||||||
|this, on_click| {
|
|
||||||
let prev_checked = state.prev_checked.clone();
|
|
||||||
this.on_mouse_down(gpui::MouseButton::Left, move |_, window, cx| {
|
|
||||||
cx.stop_propagation();
|
|
||||||
*prev_checked.borrow_mut() = Some(checked);
|
|
||||||
on_click(&!checked, window, cx);
|
|
||||||
})
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.into_any_element();
|
|
||||||
|
|
||||||
((element.request_layout(window, cx), element), state)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prepaint(
|
|
||||||
&mut self,
|
|
||||||
_: Option<&gpui::GlobalElementId>,
|
|
||||||
_: Option<&gpui::InspectorElementId>,
|
|
||||||
_: gpui::Bounds<gpui::Pixels>,
|
|
||||||
element: &mut Self::RequestLayoutState,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut App,
|
|
||||||
) {
|
|
||||||
element.prepaint(window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn paint(
|
|
||||||
&mut self,
|
|
||||||
_: Option<&gpui::GlobalElementId>,
|
|
||||||
_: Option<&gpui::InspectorElementId>,
|
|
||||||
_: gpui::Bounds<gpui::Pixels>,
|
|
||||||
element: &mut Self::RequestLayoutState,
|
|
||||||
_: &mut Self::PrepaintState,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut App,
|
|
||||||
) {
|
|
||||||
element.paint(window, cx)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, relative, App, AppContext, Context, Entity, IntoElement, ParentElement, Render,
|
App, AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled,
|
||||||
SharedString, Styled, Window,
|
Window, div, relative,
|
||||||
};
|
};
|
||||||
|
use gpui_base::Tooltip as BaseTooltip;
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
|
|
||||||
pub struct Tooltip {
|
pub struct Tooltip {
|
||||||
@@ -18,7 +19,7 @@ impl Tooltip {
|
|||||||
impl Render for Tooltip {
|
impl Render for Tooltip {
|
||||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
div().child(
|
div().child(
|
||||||
div()
|
BaseTooltip::new("tooltip")
|
||||||
.font_family(".SystemUIFont")
|
.font_family(".SystemUIFont")
|
||||||
.m_3()
|
.m_3()
|
||||||
.p_1p5()
|
.p_1p5()
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ use std::rc::Rc;
|
|||||||
use gpui::{App, ElementId, Entity, Window};
|
use gpui::{App, ElementId, Entity, Window};
|
||||||
|
|
||||||
use crate::Root;
|
use crate::Root;
|
||||||
use crate::input::InputState;
|
|
||||||
use crate::modal::Modal;
|
use crate::modal::Modal;
|
||||||
use crate::notification::Notification;
|
use crate::notification::Notification;
|
||||||
|
|
||||||
@@ -43,12 +42,6 @@ pub trait WindowExtension: Sized {
|
|||||||
|
|
||||||
/// Clear all notifications
|
/// Clear all notifications
|
||||||
fn clear_notifications(&mut self, cx: &mut App);
|
fn clear_notifications(&mut self, cx: &mut App);
|
||||||
|
|
||||||
/// Return current focused Input entity.
|
|
||||||
fn focused_input(&mut self, cx: &mut App) -> Option<Entity<InputState>>;
|
|
||||||
|
|
||||||
/// Returns true if there is a focused Input entity.
|
|
||||||
fn has_focused_input(&mut self, cx: &mut App) -> bool;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WindowExtension for Window {
|
impl WindowExtension for Window {
|
||||||
@@ -122,12 +115,4 @@ impl WindowExtension for Window {
|
|||||||
let entity = Root::read(self, cx).notification.clone();
|
let entity = Root::read(self, cx).notification.clone();
|
||||||
Rc::new(entity.read(cx).notifications())
|
Rc::new(entity.read(cx).notifications())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn has_focused_input(&mut self, cx: &mut App) -> bool {
|
|
||||||
Root::read(self, cx).focused_input.is_some()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn focused_input(&mut self, cx: &mut App) -> Option<Entity<InputState>> {
|
|
||||||
Root::read(self, cx).focused_input.clone()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+43
-48
@@ -8,7 +8,7 @@ use common::download_dir;
|
|||||||
use device::{DeviceEvent, DeviceRegistry};
|
use device::{DeviceEvent, DeviceRegistry};
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Action, App, AppContext, Axis, Context, Entity, InteractiveElement, IntoElement, ParentElement,
|
Action, App, AppContext, Context, Entity, InteractiveElement, IntoElement, ParentElement,
|
||||||
Render, SharedString, Styled, Subscription, Task, Window, div, px,
|
Render, SharedString, Styled, Subscription, Task, Window, div, px,
|
||||||
};
|
};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
@@ -19,7 +19,7 @@ use state::{NostrRegistry, StateEvent};
|
|||||||
use theme::{ActiveTheme, SIDEBAR_WIDTH, Theme, ThemeRegistry};
|
use theme::{ActiveTheme, SIDEBAR_WIDTH, Theme, ThemeRegistry};
|
||||||
use ui::avatar::Avatar;
|
use ui::avatar::Avatar;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::dock::{ClosePanel, DockArea, DockItem, DockPlacement, PanelView};
|
use ui::dock::{self, ClosePanel, DockArea, DockLayout, DockPlacement, Panel, PanelHandle};
|
||||||
use ui::menu::{DropdownMenu, PopupMenuItem};
|
use ui::menu::{DropdownMenu, PopupMenuItem};
|
||||||
use ui::notification::{Notification, NotificationKind};
|
use ui::notification::{Notification, NotificationKind};
|
||||||
use ui::{Icon, IconName, Root, Sizable, TitleBar, WindowExtension, h_flex, v_flex};
|
use ui::{Icon, IconName, Root, Sizable, TitleBar, WindowExtension, h_flex, v_flex};
|
||||||
@@ -78,7 +78,7 @@ impl Workspace {
|
|||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
|
||||||
let sidebar = cx.new(|cx| Sidebar::new(window, cx));
|
let sidebar = cx.new(|cx| Sidebar::new(window, cx));
|
||||||
let dock = cx.new(|cx| DockArea::new(window, cx));
|
let dock = dock::dock_area("coop", window, cx);
|
||||||
|
|
||||||
let mut subscriptions = smallvec![];
|
let mut subscriptions = smallvec![];
|
||||||
|
|
||||||
@@ -185,20 +185,18 @@ impl Workspace {
|
|||||||
}
|
}
|
||||||
ChatEvent::OpenRoom(id) => {
|
ChatEvent::OpenRoom(id) => {
|
||||||
if let Some(room) = chat.read(cx).room(id, cx) {
|
if let Some(room) = chat.read(cx).room(id, cx) {
|
||||||
this.dock.update(cx, |this, cx| {
|
this.add_panel_to_dock(
|
||||||
this.add_panel(
|
chat_ui::init(room, window, cx),
|
||||||
Arc::new(chat_ui::init(room, window, cx)),
|
|
||||||
DockPlacement::Center,
|
DockPlacement::Center,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ChatEvent::CloseRoom(..) => {
|
ChatEvent::CloseRoom(..) => {
|
||||||
this.dock.update(cx, |this, cx| {
|
this.dock.update(cx, |area, cx| {
|
||||||
// Force focus to the tab panel
|
// Force focus to the tab panel
|
||||||
this.focus_tab_panel(window, cx);
|
ui::dock::focus_tab_panel(area, window, cx);
|
||||||
|
|
||||||
// Dispatch the close panel action
|
// Dispatch the close panel action
|
||||||
cx.defer_in(window, |_, window, cx| {
|
cx.defer_in(window, |_, window, cx| {
|
||||||
@@ -216,14 +214,12 @@ impl Workspace {
|
|||||||
);
|
);
|
||||||
|
|
||||||
cx.defer_in(window, |this, window, cx| {
|
cx.defer_in(window, |this, window, cx| {
|
||||||
let dock = this.dock.downgrade();
|
let greeter = PanelHandle::new(greeter::init(window, cx));
|
||||||
let greeter = Arc::new(greeter::init(window, cx));
|
let center = DockLayout::v_split()
|
||||||
let tabs = DockItem::tabs(vec![greeter], None, &dock, window, cx);
|
.child(DockLayout::tabs().panel_view(Arc::new(greeter), cx), None);
|
||||||
let center = DockItem::split(Axis::Vertical, vec![tabs], &dock, window, cx);
|
|
||||||
|
|
||||||
this.dock.update(cx, |this, cx| {
|
this.dock
|
||||||
this.set_center(center, window, cx);
|
.update(cx, |area, cx| area.set_center(center, window, cx));
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
@@ -234,22 +230,36 @@ impl Workspace {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add panel to the dock
|
/// Add a panel to the dock, from anywhere that has the window but not the
|
||||||
pub fn add_panel<P>(panel: P, placement: DockPlacement, window: &mut Window, cx: &mut App)
|
/// workspace.
|
||||||
where
|
pub fn add_panel<P: Panel>(
|
||||||
P: PanelView,
|
panel: Entity<P>,
|
||||||
{
|
placement: DockPlacement,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut App,
|
||||||
|
) {
|
||||||
if let Some(root) = window.root::<Root>().flatten()
|
if let Some(root) = window.root::<Root>().flatten()
|
||||||
&& let Ok(workspace) = root.read(cx).view().clone().downcast::<Self>()
|
&& let Ok(workspace) = root.read(cx).view().clone().downcast::<Self>()
|
||||||
{
|
{
|
||||||
workspace.update(cx, |this, cx| {
|
workspace.update(cx, |this, cx| {
|
||||||
this.dock.update(cx, |this, cx| {
|
this.add_panel_to_dock(panel, placement, window, cx)
|
||||||
this.add_panel(Arc::new(panel), placement, window, cx);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Add a panel to the dock, or focus it if it is already docked.
|
||||||
|
fn add_panel_to_dock<P: Panel>(
|
||||||
|
&mut self,
|
||||||
|
panel: Entity<P>,
|
||||||
|
placement: DockPlacement,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.dock.update(cx, |area, cx| {
|
||||||
|
ui::dock::add_panel(area, PanelHandle::new(panel), placement, window, cx)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// Handle command events
|
/// Handle command events
|
||||||
fn on_command(&mut self, command: &Command, window: &mut Window, cx: &mut Context<Self>) {
|
fn on_command(&mut self, command: &Command, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
match command {
|
match command {
|
||||||
@@ -268,45 +278,32 @@ impl Workspace {
|
|||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
|
||||||
if let Some(public_key) = nostr.read(cx).current_user() {
|
if let Some(public_key) = nostr.read(cx).current_user() {
|
||||||
self.dock.update(cx, |this, cx| {
|
self.add_panel_to_dock(
|
||||||
this.add_panel(
|
profile::init(public_key, window, cx),
|
||||||
Arc::new(profile::init(public_key, window, cx)),
|
|
||||||
DockPlacement::Left,
|
DockPlacement::Left,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Command::ShowContactList => {
|
Command::ShowContactList => {
|
||||||
self.dock.update(cx, |this, cx| {
|
self.add_panel_to_dock(
|
||||||
this.add_panel(
|
contact_list::init(window, cx),
|
||||||
Arc::new(contact_list::init(window, cx)),
|
|
||||||
DockPlacement::Left,
|
DockPlacement::Left,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
Command::ShowBackup => {
|
Command::ShowBackup => {
|
||||||
self.dock.update(cx, |this, cx| {
|
self.add_panel_to_dock(backup::init(window, cx), DockPlacement::Left, window, cx);
|
||||||
this.add_panel(
|
|
||||||
Arc::new(backup::init(window, cx)),
|
|
||||||
DockPlacement::Left,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
Command::ShowMessaging => {
|
Command::ShowMessaging => {
|
||||||
self.dock.update(cx, |this, cx| {
|
self.add_panel_to_dock(
|
||||||
this.add_panel(
|
messaging_relays::init(window, cx),
|
||||||
Arc::new(messaging_relays::init(window, cx)),
|
|
||||||
DockPlacement::Left,
|
DockPlacement::Left,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
Command::RefreshMessagingRelays => {
|
Command::RefreshMessagingRelays => {
|
||||||
let chat = ChatRegistry::global(cx);
|
let chat = ChatRegistry::global(cx);
|
||||||
@@ -316,14 +313,12 @@ impl Workspace {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
Command::ShowRelayList => {
|
Command::ShowRelayList => {
|
||||||
self.dock.update(cx, |this, cx| {
|
self.add_panel_to_dock(
|
||||||
this.add_panel(
|
relay_list::init(window, cx),
|
||||||
Arc::new(relay_list::init(window, cx)),
|
|
||||||
DockPlacement::Right,
|
DockPlacement::Right,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
Command::RefreshEncryption => {
|
Command::RefreshEncryption => {
|
||||||
let device = DeviceRegistry::global(cx);
|
let device = DeviceRegistry::global(cx);
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ use theme::ActiveTheme;
|
|||||||
use ui::avatar::Avatar;
|
use ui::avatar::Avatar;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::dock::{Panel, PanelEvent};
|
use ui::dock::{Panel, PanelEvent};
|
||||||
use ui::input::{Input, InputState};
|
use ui::input::{Input, InputState, Textarea, TextareaState};
|
||||||
use ui::notification::Notification;
|
use ui::notification::Notification;
|
||||||
use ui::{Disableable, IconName, Sizable, StyledExt, WindowExtension, h_flex, v_flex};
|
use ui::{Disableable, IconName, Sizable, StyledExt, WindowExtension, h_flex, v_flex};
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ pub struct ProfilePanel {
|
|||||||
avatar_input: Entity<InputState>,
|
avatar_input: Entity<InputState>,
|
||||||
|
|
||||||
/// User's bio multi line input
|
/// User's bio multi line input
|
||||||
bio_input: Entity<InputState>,
|
bio_input: Entity<TextareaState>,
|
||||||
|
|
||||||
/// User's website url text input
|
/// User's website url text input
|
||||||
website_input: Entity<InputState>,
|
website_input: Entity<InputState>,
|
||||||
@@ -64,8 +64,7 @@ impl ProfilePanel {
|
|||||||
|
|
||||||
// Use multi-line input for bio
|
// Use multi-line input for bio
|
||||||
let bio_input = cx.new(|cx| {
|
let bio_input = cx.new(|cx| {
|
||||||
InputState::new(window, cx)
|
TextareaState::new(window, cx)
|
||||||
.multi_line(true)
|
|
||||||
.auto_grow(3, 8)
|
.auto_grow(3, 8)
|
||||||
.placeholder("A short introduce about you.")
|
.placeholder("A short introduce about you.")
|
||||||
});
|
});
|
||||||
@@ -367,7 +366,7 @@ impl Render for ProfilePanel {
|
|||||||
.text_color(cx.theme().text_muted)
|
.text_color(cx.theme().text_muted)
|
||||||
.child(SharedString::from("A short introduction about you:")),
|
.child(SharedString::from("A short introduction about you:")),
|
||||||
)
|
)
|
||||||
.child(Input::new(&self.bio_input).small()),
|
.child(Textarea::new(&self.bio_input).small()),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
|
|||||||
@@ -257,10 +257,10 @@ impl Sidebar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Set the finding status
|
/// Set the finding status
|
||||||
fn set_finding(&mut self, status: bool, _window: &mut Window, cx: &mut Context<Self>) {
|
fn set_finding(&mut self, status: bool, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
// Disable the input to prevent duplicate requests
|
// Disable the input to prevent duplicate requests
|
||||||
self.find_input.update(cx, |this, cx| {
|
self.find_input.update(cx, |this, cx| {
|
||||||
this.set_loading(status, cx);
|
this.set_loading(status, window, cx);
|
||||||
});
|
});
|
||||||
// Set the search status
|
// Set the search status
|
||||||
self.finding = status;
|
self.finding = status;
|
||||||
@@ -530,7 +530,9 @@ impl Render for Sidebar {
|
|||||||
.small()
|
.small()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
.disabled(loading)
|
.disabled(loading)
|
||||||
.when(!self.find_input.read(cx).loading, |this| {
|
.when(
|
||||||
|
!self.find_input.read(cx).presentation().is_loading(),
|
||||||
|
|this| {
|
||||||
this.suffix(
|
this.suffix(
|
||||||
Button::new("find-icon")
|
Button::new("find-icon")
|
||||||
.icon(IconName::Search)
|
.icon(IconName::Search)
|
||||||
@@ -538,7 +540,8 @@ impl Render for Sidebar {
|
|||||||
.transparent()
|
.transparent()
|
||||||
.small(),
|
.small(),
|
||||||
)
|
)
|
||||||
}),
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@ person = { path = "../crates/person" }
|
|||||||
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
gpui_platform.workspace = true
|
gpui_platform.workspace = true
|
||||||
gpui_web = { git = "https://github.com/zed-industries/zed" }
|
gpui_web.workspace = true
|
||||||
|
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
instant = { workspace = true, features = ["wasm-bindgen"] }
|
instant = { workspace = true, features = ["wasm-bindgen"] }
|
||||||
|
|||||||
Reference in New Issue
Block a user