170 lines
6.0 KiB
Rust
170 lines
6.0 KiB
Rust
use std::rc::Rc;
|
|
|
|
use gpui::{
|
|
AnimationExt, AnyElement, App, Corner, ElementId, Entity, FontWeight, InteractiveElement,
|
|
IntoElement, ParentElement, RenderOnce, SharedString, Styled, Window, anchored, canvas,
|
|
deferred, div, point, prelude::*,
|
|
};
|
|
use smallvec::SmallVec;
|
|
|
|
use crate::{
|
|
buttons::{ButtonVariant, IconButton},
|
|
forms::{Run, TextInput, surface},
|
|
layout::Collapsible,
|
|
motion,
|
|
primitives::IconName,
|
|
theme::{ActiveTheme, ControlSize, TextSize},
|
|
};
|
|
|
|
/// Tools that float over a text field's selection while it has one and holds focus. Its buttons leave focus in the field.
|
|
#[derive(IntoElement)]
|
|
pub struct FloatingToolbar {
|
|
id: ElementId,
|
|
field: Entity<TextInput>,
|
|
tools: SmallVec<[AnyElement; 4]>,
|
|
}
|
|
|
|
impl FloatingToolbar {
|
|
pub fn new(id: impl Into<ElementId>, field: &Entity<TextInput>) -> Self {
|
|
Self {
|
|
id: id.into(),
|
|
field: field.clone(),
|
|
tools: SmallVec::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ParentElement for FloatingToolbar {
|
|
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
|
self.tools.extend(elements);
|
|
}
|
|
}
|
|
|
|
impl RenderOnce for FloatingToolbar {
|
|
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
|
let seen = window.use_keyed_state((self.id.clone(), "seen"), cx, |_, _| None);
|
|
let input = self.field.read(cx);
|
|
let selection = input.selection();
|
|
let shown = !selection.is_empty() && input.focus().is_focused(window);
|
|
let start = input.bounds_for(selection.start);
|
|
let anchor = start.filter(|_| shown);
|
|
let (field, from) = (self.field.clone(), selection.start);
|
|
div()
|
|
.id(self.id.clone())
|
|
.child(canvas(
|
|
|_, _, _| {},
|
|
move |_, _, _, cx| {
|
|
let now = field.read(cx).bounds_for(from);
|
|
if now != start {
|
|
log::debug!("floating toolbar: the selection moved to {now:?}");
|
|
seen.update(cx, |seen, cx| {
|
|
*seen = now;
|
|
cx.notify();
|
|
});
|
|
}
|
|
},
|
|
))
|
|
.when_some(anchor, |host, anchor| {
|
|
let lift = cx.theme().float_gap().to_pixels(window.rem_size());
|
|
host.child(
|
|
deferred(
|
|
anchored()
|
|
.position(anchor.origin - point(gpui::Pixels::ZERO, lift))
|
|
.anchor(Corner::BottomLeft)
|
|
.snap_to_window()
|
|
.child(
|
|
surface((self.id.clone(), "bar"), cx)
|
|
.occlude()
|
|
.flex()
|
|
.items_center()
|
|
.gap_0p5()
|
|
.p_1()
|
|
.children(self.tools)
|
|
.with_animation(
|
|
(self.id.clone(), "in"),
|
|
gpui::Animation::new(motion::duration(motion::FAST, cx))
|
|
.with_easing(motion::ease_out_cubic),
|
|
|bar, t| bar.opacity(t).mb(motion::NUDGE * (1.0 - t)),
|
|
),
|
|
),
|
|
)
|
|
.with_priority(1),
|
|
)
|
|
})
|
|
}
|
|
}
|
|
|
|
/// A panel that opens in the flow under a line, the way an editor peeks at a definition: a title bar with a close button over the owner's content.
|
|
#[derive(IntoElement)]
|
|
pub struct Peek {
|
|
id: ElementId,
|
|
title: SharedString,
|
|
open: bool,
|
|
body: SmallVec<[AnyElement; 2]>,
|
|
on_close: Run,
|
|
}
|
|
|
|
impl Peek {
|
|
pub fn new(
|
|
id: impl Into<ElementId>,
|
|
title: impl Into<SharedString>,
|
|
open: bool,
|
|
on_close: impl Fn(&mut Window, &mut App) + 'static,
|
|
) -> Self {
|
|
Self {
|
|
id: id.into(),
|
|
title: title.into(),
|
|
open,
|
|
body: SmallVec::new(),
|
|
on_close: Rc::new(on_close),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ParentElement for Peek {
|
|
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
|
self.body.extend(elements);
|
|
}
|
|
}
|
|
|
|
impl RenderOnce for Peek {
|
|
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
|
let theme = cx.theme();
|
|
let colors = &theme.colors;
|
|
let (id, close) = (self.id.clone(), self.on_close);
|
|
Collapsible::new(self.id.clone(), self.open).child(
|
|
div()
|
|
.my_2()
|
|
.border_l_2()
|
|
.border_color(colors.accent)
|
|
.bg(colors.surface)
|
|
.child(
|
|
div()
|
|
.flex()
|
|
.items_center()
|
|
.justify_between()
|
|
.gap_2()
|
|
.h(theme.control_height(ControlSize::Lg))
|
|
.pl_3()
|
|
.pr_1()
|
|
.border_b_1()
|
|
.border_color(colors.border)
|
|
.text_size(theme.text_size(TextSize::Sm))
|
|
.font_weight(FontWeight::MEDIUM)
|
|
.child(self.title)
|
|
.child(
|
|
IconButton::new((self.id.clone(), "close"), IconName::X)
|
|
.variant(ButtonVariant::Ghost)
|
|
.size(ControlSize::Sm)
|
|
.tooltip("Close")
|
|
.on_click(move |_, window, cx| {
|
|
log::info!("peek {id:?}: closed");
|
|
close(window, cx)
|
|
}),
|
|
),
|
|
)
|
|
.child(div().p_3().children(self.body)),
|
|
)
|
|
}
|
|
}
|