feat: add contact-based chat creation and re-add chat screening (#54)
Reviewed-on: reya/coop#54
This commit is contained in:
+24
-132
@@ -10,7 +10,10 @@ use crate::{Selectable, Sizable, Size, StyledExt};
|
||||
|
||||
/// Number of rows and columns in the generated pixel grid.
|
||||
const PIXEL_GRID: usize = 8;
|
||||
/// Probability that a cell in the left half of the grid is filled.
|
||||
/// Empty cells kept between the pattern and the avatar edge, so the art
|
||||
/// gathers in the center instead of filling the whole avatar.
|
||||
const MARGIN: usize = 1;
|
||||
/// Probability that a cell in the left half of the pattern area is filled.
|
||||
const FILL_PROBABILITY: f32 = 0.42;
|
||||
/// Probability that a filled cell uses the accent shade instead of the main color.
|
||||
const ACCENT_PROBABILITY: f32 = 0.25;
|
||||
@@ -33,19 +36,6 @@ pub(super) fn avatar_size(size: Size) -> AbsoluteLength {
|
||||
}
|
||||
|
||||
/// A deterministic, offline pixel-art avatar derived from a seed.
|
||||
///
|
||||
/// Use it for entities that have no profile picture: the same seed always
|
||||
/// renders the same pattern, so identities stay recognizable without a
|
||||
/// network round trip. The pattern is painted as geometry and cropped to a
|
||||
/// circle, at the same sizes as [`Avatar`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ui::avatar::PixelAvatar;
|
||||
///
|
||||
/// PixelAvatar::new("alice");
|
||||
/// ```
|
||||
#[derive(IntoElement)]
|
||||
pub struct PixelAvatar {
|
||||
seed: u64,
|
||||
@@ -87,7 +77,7 @@ impl RenderOnce for PixelAvatar {
|
||||
move |bounds, seed, window, cx| {
|
||||
let theme = cx.theme();
|
||||
let main = Hsla {
|
||||
h: (theme.icon_accent.h + seed as f32 / u64::MAX as f32) % 1.,
|
||||
h: seed as f32 / u64::MAX as f32,
|
||||
s: 0.6,
|
||||
l: if theme.is_dark() { 0.6 } else { 0.45 },
|
||||
a: 1.,
|
||||
@@ -142,8 +132,11 @@ fn pixel_pattern(seed: u64) -> [u8; PIXEL_GRID * PIXEL_GRID] {
|
||||
let mut pattern = [0u8; PIXEL_GRID * PIXEL_GRID];
|
||||
let mut filled = 0usize;
|
||||
|
||||
for row in 0..PIXEL_GRID {
|
||||
for col in 0..PIXEL_GRID / 2 {
|
||||
let art_rows = PIXEL_GRID - 2 * MARGIN;
|
||||
let art_columns = PIXEL_GRID / 2 - MARGIN;
|
||||
|
||||
for row in MARGIN..PIXEL_GRID - MARGIN {
|
||||
for col in MARGIN..PIXEL_GRID / 2 {
|
||||
if rng.chance(FILL_PROBABILITY) {
|
||||
let accent = rng.chance(ACCENT_PROBABILITY);
|
||||
set_cell(&mut pattern, row, col, if accent { 2 } else { 1 });
|
||||
@@ -153,17 +146,17 @@ fn pixel_pattern(seed: u64) -> [u8; PIXEL_GRID * PIXEL_GRID] {
|
||||
}
|
||||
|
||||
if filled < MIN_FILLED {
|
||||
let half = PIXEL_GRID * PIXEL_GRID / 2;
|
||||
let start = (rng.next() % half as u64) as usize;
|
||||
let total = art_rows * art_columns;
|
||||
let start = (rng.next() % total as u64) as usize;
|
||||
|
||||
for offset in 0..half {
|
||||
for offset in 0..total {
|
||||
if filled >= MIN_FILLED {
|
||||
break;
|
||||
}
|
||||
|
||||
let ix = (start + offset) % half;
|
||||
let row = ix / (PIXEL_GRID / 2);
|
||||
let col = ix % (PIXEL_GRID / 2);
|
||||
let ix = (start + offset) % total;
|
||||
let row = MARGIN + ix / art_columns;
|
||||
let col = MARGIN + ix % art_columns;
|
||||
|
||||
if pattern[row * PIXEL_GRID + col] == 0 {
|
||||
set_cell(&mut pattern, row, col, 1);
|
||||
@@ -358,24 +351,12 @@ fn generated_avatar(seed: Option<&str>, size: Pixels) -> AnyElement {
|
||||
}
|
||||
|
||||
/// An element that renders a user avatar with customizable appearance options.
|
||||
///
|
||||
/// Entities without a picture still get a stable identity: the avatar falls
|
||||
/// back to a [`PixelAvatar`] seeded through [`Avatar::seed`], both when there
|
||||
/// is no picture and when the picture fails to load.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ui::avatar::Avatar;
|
||||
///
|
||||
/// Avatar::new(None).seed("alice");
|
||||
/// ```
|
||||
#[derive(IntoElement)]
|
||||
pub struct Avatar {
|
||||
base: Div,
|
||||
picture: Option<ImageSource>,
|
||||
grayscale: bool,
|
||||
seed: Option<SharedString>,
|
||||
seed: Option<String>,
|
||||
style: StyleRefinement,
|
||||
size: Size,
|
||||
border_color: Option<Hsla>,
|
||||
@@ -384,16 +365,13 @@ pub struct Avatar {
|
||||
|
||||
impl Avatar {
|
||||
/// Creates an avatar for an entity whose profile picture may be missing.
|
||||
///
|
||||
/// Use [`Avatar::seed`] to choose the generated
|
||||
/// pixel avatar rendered when `picture` is `None`.
|
||||
pub fn new(picture: Option<SharedString>) -> Self {
|
||||
Self::from_picture(picture.map(ImageSource::from))
|
||||
Self::from_source(picture)
|
||||
}
|
||||
|
||||
/// Creates an avatar from an already-resolved source.
|
||||
pub fn from_source(picture: impl Into<ImageSource>) -> Self {
|
||||
Self::from_picture(Some(picture.into()))
|
||||
/// Creates an avatar from an already-resolved source, which may be missing.
|
||||
pub fn from_source(picture: Option<impl Into<ImageSource>>) -> Self {
|
||||
Self::from_picture(picture.map(Into::into))
|
||||
}
|
||||
|
||||
fn from_picture(picture: Option<ImageSource>) -> Self {
|
||||
@@ -410,33 +388,18 @@ impl Avatar {
|
||||
}
|
||||
|
||||
/// Sets the seed for the generated pixel avatar.
|
||||
///
|
||||
/// The seed should be a stable identifier of the entity the avatar
|
||||
/// represents, such as a public key.
|
||||
pub fn seed(mut self, seed: impl Into<SharedString>) -> Self {
|
||||
pub fn seed(mut self, seed: impl Into<String>) -> Self {
|
||||
self.seed = Some(seed.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Applies a grayscale filter to the avatar image.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ui::avatar::Avatar;
|
||||
///
|
||||
/// Avatar::new(None).grayscale(true);
|
||||
/// ```
|
||||
pub fn grayscale(mut self, grayscale: bool) -> Self {
|
||||
self.grayscale = grayscale;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the border color of the avatar.
|
||||
///
|
||||
/// This might be used to match the border to the background color of
|
||||
/// the parent element to create the illusion of cropping another
|
||||
/// shape underneath (for example in face piles.)
|
||||
pub fn border_color(mut self, color: impl Into<Hsla>) -> Self {
|
||||
self.border_color = Some(color.into());
|
||||
self
|
||||
@@ -480,6 +443,7 @@ impl RenderOnce for Avatar {
|
||||
} else {
|
||||
px(0.)
|
||||
};
|
||||
|
||||
let image_size = avatar_size(self.size).to_pixels(window.rem_size());
|
||||
let container_size = image_size + border_width * 2.;
|
||||
|
||||
@@ -487,6 +451,7 @@ impl RenderOnce for Avatar {
|
||||
Some(picture) => {
|
||||
let seed = self.seed;
|
||||
let grayscale = self.grayscale;
|
||||
|
||||
img(picture)
|
||||
.size(image_size)
|
||||
.rounded_full()
|
||||
@@ -510,76 +475,3 @@ impl RenderOnce for Avatar {
|
||||
.child(content)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn pixel_patterns_are_symmetric_and_stable() {
|
||||
for seed in 0..50 {
|
||||
let pattern = pixel_pattern(seed);
|
||||
let filled = pattern.iter().filter(|&&cell| cell != 0).count();
|
||||
|
||||
assert!(
|
||||
filled >= MIN_FILLED * 2,
|
||||
"pattern too sparse for seed {seed}"
|
||||
);
|
||||
|
||||
for row in 0..PIXEL_GRID {
|
||||
for col in 0..PIXEL_GRID {
|
||||
assert_eq!(
|
||||
pattern[row * PIXEL_GRID + col],
|
||||
pattern[row * PIXEL_GRID + (PIXEL_GRID - 1 - col)],
|
||||
"asymmetric pattern for seed {seed} at ({row}, {col})"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for seed in [0, 1, 42, u64::MAX] {
|
||||
assert_eq!(pixel_pattern(seed), pixel_pattern(seed));
|
||||
}
|
||||
|
||||
assert_ne!(pixel_pattern(42), pixel_pattern(43));
|
||||
}
|
||||
|
||||
fn area(polygon: &[Point<Pixels>]) -> f32 {
|
||||
let mut sum: f32 = 0.;
|
||||
for (&a, &b) in polygon.iter().zip(polygon.iter().cycle().skip(1)) {
|
||||
sum += a.x.as_f32() * b.y.as_f32() - b.x.as_f32() * a.y.as_f32();
|
||||
}
|
||||
(sum / 2.).abs()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clipping_keeps_only_the_part_inside_the_circle() {
|
||||
let circle = circle_polygon(point(px(10.), px(10.)), 10.);
|
||||
let square = |left: f32, top: f32| {
|
||||
[
|
||||
point(px(left), px(top)),
|
||||
point(px(left + 4.), px(top)),
|
||||
point(px(left + 4.), px(top + 4.)),
|
||||
point(px(left), px(top + 4.)),
|
||||
]
|
||||
};
|
||||
|
||||
let inside = clip_polygon(&square(8., 8.), &circle);
|
||||
assert!((area(&inside) - 16.).abs() < 0.05, "area {}", area(&inside));
|
||||
|
||||
assert!(clip_polygon(&square(20., 20.), &circle).is_empty());
|
||||
|
||||
let straddling = clip_polygon(&square(0., 0.), &circle);
|
||||
for vertex in &straddling {
|
||||
let delta_x = vertex.x.as_f32() - 10.;
|
||||
let delta_y = vertex.y.as_f32() - 10.;
|
||||
assert!(
|
||||
delta_x.hypot(delta_y) <= 10. + 0.1,
|
||||
"clipped vertex outside the circle"
|
||||
);
|
||||
}
|
||||
|
||||
let area = area(&straddling);
|
||||
assert!(area > 0. && area < 16., "area {area}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ use gpui::{
|
||||
InteractiveElement as _, IntoElement, ParentElement, Pixels, RenderOnce, SharedString,
|
||||
StyleRefinement, Styled, Window, div, hsla, point, px, size,
|
||||
};
|
||||
use gpui_base::Dialog;
|
||||
use instant::Duration;
|
||||
use theme::ActiveTheme;
|
||||
|
||||
@@ -22,15 +21,15 @@ type RenderButtonFn = Box<dyn FnOnce(&mut Window, &mut App) -> AnyElement>;
|
||||
type FooterFn =
|
||||
Box<dyn Fn(RenderButtonFn, RenderButtonFn, &mut Window, &mut App) -> Vec<AnyElement>>;
|
||||
|
||||
/// Modal button props.
|
||||
pub struct ModalButtonProps {
|
||||
/// Dialog button props.
|
||||
pub struct DialogButtonProps {
|
||||
ok_text: Option<SharedString>,
|
||||
ok_variant: ButtonVariant,
|
||||
cancel_text: Option<SharedString>,
|
||||
cancel_variant: ButtonVariant,
|
||||
}
|
||||
|
||||
impl Default for ModalButtonProps {
|
||||
impl Default for DialogButtonProps {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
ok_text: None,
|
||||
@@ -41,7 +40,7 @@ impl Default for ModalButtonProps {
|
||||
}
|
||||
}
|
||||
|
||||
impl ModalButtonProps {
|
||||
impl DialogButtonProps {
|
||||
/// Sets the text of the OK button. Default is `OK`.
|
||||
pub fn ok_text(mut self, ok_text: impl Into<SharedString>) -> Self {
|
||||
self.ok_text = Some(ok_text.into());
|
||||
@@ -68,7 +67,7 @@ impl ModalButtonProps {
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct Modal {
|
||||
pub struct Dialog {
|
||||
style: StyleRefinement,
|
||||
title: Option<AnyElement>,
|
||||
footer: Option<FooterFn>,
|
||||
@@ -85,15 +84,15 @@ pub struct Modal {
|
||||
overlay_closable: bool,
|
||||
keyboard: bool,
|
||||
show_close: bool,
|
||||
button_props: ModalButtonProps,
|
||||
button_props: DialogButtonProps,
|
||||
|
||||
/// This will be change when open the modal, the focus handle is create when open the modal.
|
||||
/// This will be change when open the dialog, the focus handle is create when open the dialog.
|
||||
pub focus_handle: FocusHandle,
|
||||
pub layer_ix: usize,
|
||||
pub overlay_visible: bool,
|
||||
}
|
||||
|
||||
impl Modal {
|
||||
impl Dialog {
|
||||
pub fn new(_window: &mut Window, cx: &mut App) -> Self {
|
||||
Self {
|
||||
style: StyleRefinement::default(),
|
||||
@@ -111,19 +110,19 @@ impl Modal {
|
||||
on_close: Rc::new(|_, _, _| {}),
|
||||
on_ok: None,
|
||||
on_cancel: Rc::new(|_, _, _| true),
|
||||
button_props: ModalButtonProps::default(),
|
||||
button_props: DialogButtonProps::default(),
|
||||
show_close: true,
|
||||
overlay_closable: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the title of the modal.
|
||||
/// Sets the title of the dialog.
|
||||
pub fn title(mut self, title: impl IntoElement) -> Self {
|
||||
self.title = Some(title.into_any_element());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the footer of the modal.
|
||||
/// Set the footer of the dialog.
|
||||
///
|
||||
/// The `footer` is a function that takes two `RenderButtonFn` and a `WindowContext` and returns a list of `AnyElement`.
|
||||
///
|
||||
@@ -145,7 +144,7 @@ impl Modal {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set to use confirm modal, with OK and Cancel buttons.
|
||||
/// Set to use confirm dialog, with OK and Cancel buttons.
|
||||
///
|
||||
/// See also [`Self::alert`]
|
||||
pub fn confirm(self) -> Self {
|
||||
@@ -154,7 +153,7 @@ impl Modal {
|
||||
.show_close(false)
|
||||
}
|
||||
|
||||
/// Set to as a alter modal, with OK button.
|
||||
/// Set to as a alter dialog, with OK button.
|
||||
///
|
||||
/// See also [`Self::confirm`]
|
||||
pub fn alert(self) -> Self {
|
||||
@@ -163,13 +162,13 @@ impl Modal {
|
||||
.show_close(false)
|
||||
}
|
||||
|
||||
/// Set the button props of the modal.
|
||||
pub fn button_props(mut self, button_props: ModalButtonProps) -> Self {
|
||||
/// Set the button props of the dialog.
|
||||
pub fn button_props(mut self, button_props: DialogButtonProps) -> Self {
|
||||
self.button_props = button_props;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the callback for when the modal is closed.
|
||||
/// Sets the callback for when the dialog is closed.
|
||||
///
|
||||
/// Called after [`Self::on_ok`] or [`Self::on_cancel`] callback.
|
||||
pub fn on_close(
|
||||
@@ -180,9 +179,9 @@ impl Modal {
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the callback for when the modal is has been confirmed.
|
||||
/// Sets the callback for when the dialog is has been confirmed.
|
||||
///
|
||||
/// The callback should return `true` to close the modal, if return `false` the modal will not be closed.
|
||||
/// The callback should return `true` to close the dialog, if return `false` the dialog will not be closed.
|
||||
pub fn on_ok(
|
||||
mut self,
|
||||
on_ok: impl Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static,
|
||||
@@ -191,9 +190,9 @@ impl Modal {
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the callback for when the modal is has been canceled.
|
||||
/// Sets the callback for when the dialog is has been canceled.
|
||||
///
|
||||
/// The callback should return `true` to close the modal, if return `false` the modal will not be closed.
|
||||
/// The callback should return `true` to close the dialog, if return `false` the dialog will not be closed.
|
||||
pub fn on_cancel(
|
||||
mut self,
|
||||
on_cancel: impl Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static,
|
||||
@@ -208,39 +207,39 @@ impl Modal {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the top offset of the modal, defaults to None, will use the 1/10 of the viewport height.
|
||||
/// Shift the dialog down from the window centre, defaults to `None`.
|
||||
pub fn margin_top(mut self, margin_top: Pixels) -> Self {
|
||||
self.margin_top = Some(margin_top);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the width of the modal, defaults to 480px.
|
||||
/// Sets the width of the dialog, defaults to 480px.
|
||||
pub fn width(mut self, width: Pixels) -> Self {
|
||||
self.width = width;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the maximum width of the modal, defaults to `None`.
|
||||
/// Set the maximum width of the dialog, defaults to `None`.
|
||||
pub fn max_w(mut self, max_width: Pixels) -> Self {
|
||||
self.max_width = Some(max_width);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the overlay of the modal, defaults to `true`.
|
||||
/// Set the overlay of the dialog, defaults to `true`.
|
||||
pub fn overlay(mut self, overlay: bool) -> Self {
|
||||
self.overlay = overlay;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the overlay closable of the modal, defaults to `true`.
|
||||
/// Set the overlay closable of the dialog, defaults to `true`.
|
||||
///
|
||||
/// When the overlay is clicked, the modal will be closed.
|
||||
/// When the overlay is clicked, the dialog will be closed.
|
||||
pub fn overlay_closable(mut self, overlay_closable: bool) -> Self {
|
||||
self.overlay_closable = overlay_closable;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set whether to support keyboard esc to close the modal, defaults to `true`.
|
||||
/// Set whether to support keyboard esc to close the dialog, defaults to `true`.
|
||||
pub fn keyboard(mut self, keyboard: bool) -> Self {
|
||||
self.keyboard = keyboard;
|
||||
self
|
||||
@@ -251,22 +250,22 @@ impl Modal {
|
||||
}
|
||||
}
|
||||
|
||||
impl ParentElement for Modal {
|
||||
impl ParentElement for Dialog {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.content.extend(elements);
|
||||
}
|
||||
}
|
||||
|
||||
impl Styled for Modal {
|
||||
impl Styled for Dialog {
|
||||
fn style(&mut self) -> &mut gpui::StyleRefinement {
|
||||
&mut self.style
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Modal {
|
||||
impl RenderOnce for Dialog {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl gpui::IntoElement {
|
||||
let layer_ix = self.layer_ix;
|
||||
let is_topmost = layer_ix + 1 == Root::read(window, cx).active_modals.len();
|
||||
let is_topmost = layer_ix + 1 == Root::read(window, cx).active_dialogs.len();
|
||||
let has_footer = self.footer.is_some();
|
||||
let on_close = self.on_close.clone();
|
||||
let on_ok = self.on_ok.clone();
|
||||
@@ -296,7 +295,7 @@ impl RenderOnce for Modal {
|
||||
}
|
||||
|
||||
on_close(&ClickEvent::default(), window, cx);
|
||||
window.close_modal(cx);
|
||||
window.close_dialog(cx);
|
||||
}
|
||||
})
|
||||
.into_any_element()
|
||||
@@ -327,7 +326,7 @@ impl RenderOnce for Modal {
|
||||
}
|
||||
|
||||
on_close(&ClickEvent::default(), window, cx);
|
||||
window.close_modal(cx);
|
||||
window.close_dialog(cx);
|
||||
}
|
||||
})
|
||||
.into_any_element()
|
||||
@@ -344,10 +343,7 @@ impl RenderOnce for Modal {
|
||||
);
|
||||
|
||||
let offset_top = px(layer_ix as f32 * 16.);
|
||||
let y = self.margin_top.unwrap_or(view_size.height / 10.) + offset_top;
|
||||
let x = view_size.width / 2. - self.width / 2.;
|
||||
let card_top = window_paddings.top + y;
|
||||
let card_left = window_paddings.left + x;
|
||||
let top_offset = self.margin_top.unwrap_or(px(0.)) + offset_top;
|
||||
|
||||
let mut padding_right = px(16.);
|
||||
let mut padding_left = px(16.);
|
||||
@@ -386,11 +382,9 @@ impl RenderOnce for Modal {
|
||||
.min_h_24()
|
||||
.refine_style(&self.style)
|
||||
// There style is high priority, can't be overridden.
|
||||
.absolute()
|
||||
.occlude()
|
||||
.relative()
|
||||
.left(card_left)
|
||||
.top(card_top)
|
||||
.top(top_offset)
|
||||
.w(self.width)
|
||||
.when_some(self.max_width, |this, w| this.max_w(w))
|
||||
.child(
|
||||
@@ -401,34 +395,41 @@ impl RenderOnce for Modal {
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.relative()
|
||||
.when_some(self.title, |this, title| {
|
||||
this.h_10().font_semibold().text_center().child(title)
|
||||
})
|
||||
.when(self.show_close, |this| {
|
||||
let on_cancel = on_cancel.clone();
|
||||
let on_close = on_close.clone();
|
||||
|
||||
this.child(
|
||||
div()
|
||||
.absolute()
|
||||
.top_0()
|
||||
.bottom_0()
|
||||
.right_2()
|
||||
.flex()
|
||||
.items_center()
|
||||
.child(
|
||||
Button::new("close")
|
||||
.icon(IconName::CloseCircleFill)
|
||||
.custom(
|
||||
ButtonCustomVariant::new(window, cx)
|
||||
.foreground(cx.theme().icon_muted)
|
||||
.color(cx.theme().ghost_element_background)
|
||||
.hover(cx.theme().ghost_element_background)
|
||||
.active(cx.theme().ghost_element_background),
|
||||
)
|
||||
.on_click(move |_, window, cx| {
|
||||
on_cancel(&ClickEvent::default(), window, cx);
|
||||
on_close(&ClickEvent::default(), window, cx);
|
||||
window.close_dialog(cx);
|
||||
}),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.when(self.show_close, |this| {
|
||||
let on_cancel = on_cancel.clone();
|
||||
let on_close = on_close.clone();
|
||||
|
||||
this.child(
|
||||
Button::new("close")
|
||||
.icon(IconName::CloseCircleFill)
|
||||
.absolute()
|
||||
.top_1p5()
|
||||
.right_2()
|
||||
.custom(
|
||||
ButtonCustomVariant::new(window, cx)
|
||||
.foreground(cx.theme().icon_muted)
|
||||
.color(cx.theme().ghost_element_background)
|
||||
.hover(cx.theme().ghost_element_background)
|
||||
.active(cx.theme().ghost_element_background),
|
||||
)
|
||||
.on_click(move |_, window, cx| {
|
||||
on_cancel(&ClickEvent::default(), window, cx);
|
||||
on_close(&ClickEvent::default(), window, cx);
|
||||
window.close_modal(cx);
|
||||
}),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.pt_px()
|
||||
@@ -459,7 +460,8 @@ impl RenderOnce for Modal {
|
||||
)
|
||||
})
|
||||
.with_animation("slide-down", animation, move |this, delta| {
|
||||
let y_offset = px(0.) + delta * px(30.);
|
||||
// Settle at the target offset; only the entrance is animated.
|
||||
let y_offset = px(30.) * (delta - 1.);
|
||||
// This is equivalent to `shadow_xl` with an extra opacity.
|
||||
let shadow = vec![
|
||||
BoxShadow {
|
||||
@@ -477,10 +479,10 @@ impl RenderOnce for Modal {
|
||||
inset: false,
|
||||
},
|
||||
];
|
||||
this.top(card_top + y_offset).shadow(shadow)
|
||||
this.top(top_offset + y_offset).shadow(shadow)
|
||||
});
|
||||
|
||||
Dialog::new(cx)
|
||||
gpui_base::Dialog::new(cx)
|
||||
.layer(layer_ix, is_topmost)
|
||||
.focus_handle(self.focus_handle.clone())
|
||||
.close_on_escape(self.keyboard)
|
||||
@@ -492,7 +494,7 @@ impl RenderOnce for Modal {
|
||||
.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);
|
||||
window.close_dialog(cx);
|
||||
})
|
||||
.backdrop(backdrop)
|
||||
.popup(card)
|
||||
@@ -16,7 +16,7 @@ use gpui_base::dock::{
|
||||
DockAreaRenderer, DockContext, DragPanel, DropIndicator, InsertTarget, NodeId, PaneNode,
|
||||
PaneRef, PanelId, TabGroupContext, TabGroupRenderer,
|
||||
};
|
||||
use gpui_base::{Placement, ResizeHandleContext, Side};
|
||||
use gpui_base::{HandleEdge, Placement, ResizeHandleContext};
|
||||
use theme::{ActiveTheme, TABBAR_HEIGHT};
|
||||
|
||||
use crate::button::{Button, ButtonVariants as _};
|
||||
@@ -225,12 +225,14 @@ impl DockSkin {
|
||||
DockPlacement::Center => "dock-resize-handle-center",
|
||||
};
|
||||
|
||||
let edge = match placement {
|
||||
DockPlacement::Left => Some(HandleEdge::Trailing),
|
||||
DockPlacement::Right | DockPlacement::Bottom => Some(HandleEdge::Leading),
|
||||
DockPlacement::Center => None,
|
||||
};
|
||||
|
||||
resize_handle(id, placement.axis())
|
||||
.placement(if placement.is_left() {
|
||||
Side::Left
|
||||
} else {
|
||||
Side::Right
|
||||
})
|
||||
.when_some(edge, |this, edge| this.inside(edge))
|
||||
.with_appearance(resize_handle_appearance())
|
||||
.on_drag(DockResizeHandle, move |info, _, _, cx| {
|
||||
cx.stop_propagation();
|
||||
|
||||
@@ -11,6 +11,7 @@ pub use crate::Disableable;
|
||||
pub mod animation;
|
||||
pub mod avatar;
|
||||
pub mod button;
|
||||
pub mod dialog;
|
||||
pub mod divider;
|
||||
pub mod dock;
|
||||
pub mod group_box;
|
||||
@@ -19,7 +20,6 @@ pub mod input;
|
||||
pub mod markdown;
|
||||
pub mod menu;
|
||||
pub mod message;
|
||||
pub mod modal;
|
||||
pub mod nav;
|
||||
pub mod nav_item;
|
||||
pub mod notification;
|
||||
|
||||
+31
-35
@@ -84,40 +84,36 @@ impl RenderOnce for Nav {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let clickable = self.on_click.is_some();
|
||||
|
||||
v_flex()
|
||||
.id(format!("nav-{}", self.id))
|
||||
.w_full()
|
||||
.h_10()
|
||||
.child(
|
||||
h_flex()
|
||||
.id(self.id)
|
||||
.h_9()
|
||||
.w_full()
|
||||
.px_1p5()
|
||||
.gap_1p5()
|
||||
.rounded(cx.theme().radius_lg)
|
||||
.when_some(self.prefix, |this, prefix| this.child(prefix))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.flex_1()
|
||||
.child(div().truncate().min_w_0().child(self.label))
|
||||
.child(div().flex_1())
|
||||
.when_some(self.suffix, |this, suffix| {
|
||||
this.child(div().flex_shrink_0().child(suffix))
|
||||
}),
|
||||
)
|
||||
.when(clickable, |this| {
|
||||
this.cursor_pointer()
|
||||
.hover(|this| this.bg(cx.theme().ghost_element_hover))
|
||||
.when(self.selected, |this| {
|
||||
this.bg(cx.theme().ghost_element_active)
|
||||
})
|
||||
})
|
||||
.when_some(self.on_click, |this, handler| {
|
||||
this.on_click(move |event, window, cx| handler(event, window, cx))
|
||||
})
|
||||
.refine_style(&self.style),
|
||||
)
|
||||
v_flex().w_full().h_10().child(
|
||||
h_flex()
|
||||
.id(self.id)
|
||||
.h_9()
|
||||
.w_full()
|
||||
.px_1p5()
|
||||
.gap_1p5()
|
||||
.rounded(cx.theme().radius_lg)
|
||||
.when_some(self.prefix, |this, prefix| this.child(prefix))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.child(div().flex_1().min_w_0().truncate().child(self.label))
|
||||
.when_some(self.suffix, |this, suffix| {
|
||||
this.child(div().flex_shrink_0().child(suffix))
|
||||
}),
|
||||
)
|
||||
.when(clickable, |this| {
|
||||
this.cursor_pointer()
|
||||
.hover(|this| this.bg(cx.theme().ghost_element_hover))
|
||||
.when(self.selected, |this| {
|
||||
this.bg(cx.theme().ghost_element_active)
|
||||
})
|
||||
})
|
||||
.when_some(self.on_click, |this, handler| {
|
||||
this.on_click(move |event, window, cx| handler(event, window, cx))
|
||||
})
|
||||
.refine_style(&self.style),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+43
-43
@@ -13,23 +13,23 @@ use theme::{
|
||||
CLIENT_SIDE_DECORATION_SHADOW,
|
||||
};
|
||||
|
||||
use crate::modal::Modal;
|
||||
use crate::dialog::Dialog;
|
||||
use crate::notification::{Notification, NotificationList};
|
||||
|
||||
#[derive(Clone)]
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub struct ActiveModal {
|
||||
pub struct ActiveDialog {
|
||||
focus_handle: FocusHandle,
|
||||
/// The previous focused handle before opening the modal.
|
||||
/// The previous focused handle before opening the dialog.
|
||||
previous_focused_handle: Option<WeakFocusHandle>,
|
||||
builder: Rc<dyn Fn(Modal, &mut Window, &mut App) -> Modal + 'static>,
|
||||
builder: Rc<dyn Fn(Dialog, &mut Window, &mut App) -> Dialog + 'static>,
|
||||
}
|
||||
|
||||
impl ActiveModal {
|
||||
impl ActiveDialog {
|
||||
fn new(
|
||||
focus_handle: FocusHandle,
|
||||
previous_focused_handle: Option<WeakFocusHandle>,
|
||||
builder: impl Fn(Modal, &mut Window, &mut App) -> Modal + 'static,
|
||||
builder: impl Fn(Dialog, &mut Window, &mut App) -> Dialog + 'static,
|
||||
) -> Self {
|
||||
Self {
|
||||
focus_handle,
|
||||
@@ -41,10 +41,10 @@ impl ActiveModal {
|
||||
|
||||
/// Root is a view for the App window for as the top level view (Must be the first view in the window).
|
||||
///
|
||||
/// It is used to manage the Modal, and Notification.
|
||||
/// It is used to manage the Dialog, and Notification.
|
||||
pub struct Root {
|
||||
/// All active models
|
||||
pub(crate) active_modals: Vec<ActiveModal>,
|
||||
/// All active dialogs
|
||||
pub(crate) active_dialogs: Vec<ActiveDialog>,
|
||||
|
||||
/// Notification layer
|
||||
pub(crate) notification: Entity<NotificationList>,
|
||||
@@ -56,7 +56,7 @@ pub struct Root {
|
||||
impl Root {
|
||||
pub fn new(view: AnyView, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
Self {
|
||||
active_modals: Vec::new(),
|
||||
active_dialogs: Vec::new(),
|
||||
notification: cx.new(|cx| NotificationList::new(window, cx)),
|
||||
view,
|
||||
}
|
||||
@@ -98,63 +98,63 @@ impl Root {
|
||||
)
|
||||
}
|
||||
|
||||
/// Render the modal layer.
|
||||
pub fn render_modal_layer(
|
||||
/// Render the dialog layer.
|
||||
pub fn render_dialog_layer(
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<impl IntoElement + use<>> {
|
||||
let root = window.root::<Root>()??;
|
||||
let active_modals = root.read(cx).active_modals.clone();
|
||||
let active_dialogs = root.read(cx).active_dialogs.clone();
|
||||
|
||||
if active_modals.is_empty() {
|
||||
if active_dialogs.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut show_overlay_ix = None;
|
||||
|
||||
let mut modals = active_modals
|
||||
let mut dialogs = active_dialogs
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, active_modal)| {
|
||||
let mut modal = Modal::new(window, cx);
|
||||
.map(|(i, active_dialog)| {
|
||||
let mut dialog = Dialog::new(window, cx);
|
||||
|
||||
modal = (active_modal.builder)(modal, window, cx);
|
||||
dialog = (active_dialog.builder)(dialog, window, cx);
|
||||
|
||||
// Give the modal the focus handle, because `modal` is a temporary value, is not possible to
|
||||
// keep the focus handle in the modal.
|
||||
// Give the dialog the focus handle, because `dialog` is a temporary value, is not possible to
|
||||
// keep the focus handle in the dialog.
|
||||
//
|
||||
// So we keep the focus handle in the `active_modal`, this is owned by the `Root`.
|
||||
modal.focus_handle = active_modal.focus_handle.clone();
|
||||
// So we keep the focus handle in the `active_dialog`, this is owned by the `Root`.
|
||||
dialog.focus_handle = active_dialog.focus_handle.clone();
|
||||
|
||||
modal.layer_ix = i;
|
||||
// Find the modal which one needs to show overlay.
|
||||
if modal.has_overlay() {
|
||||
dialog.layer_ix = i;
|
||||
// Find the dialog which one needs to show overlay.
|
||||
if dialog.has_overlay() {
|
||||
show_overlay_ix = Some(i);
|
||||
}
|
||||
|
||||
modal
|
||||
dialog
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if let Some(ix) = show_overlay_ix
|
||||
&& let Some(modal) = modals.get_mut(ix)
|
||||
&& let Some(dialog) = dialogs.get_mut(ix)
|
||||
{
|
||||
modal.overlay_visible = true;
|
||||
dialog.overlay_visible = true;
|
||||
}
|
||||
|
||||
Some(div().children(modals))
|
||||
Some(div().children(dialogs))
|
||||
}
|
||||
|
||||
/// Open a modal.
|
||||
pub fn open_modal<F>(&mut self, builder: F, window: &mut Window, cx: &mut Context<'_, Self>)
|
||||
/// Open a dialog.
|
||||
pub fn open_dialog<F>(&mut self, builder: F, window: &mut Window, cx: &mut Context<'_, Self>)
|
||||
where
|
||||
F: Fn(Modal, &mut Window, &mut App) -> Modal + 'static,
|
||||
F: Fn(Dialog, &mut Window, &mut App) -> Dialog + 'static,
|
||||
{
|
||||
let previous_focused_handle = window.focused(cx).map(|h| h.downgrade());
|
||||
let focus_handle = cx.focus_handle();
|
||||
focus_handle.focus(window, cx);
|
||||
|
||||
self.active_modals.push(ActiveModal::new(
|
||||
self.active_dialogs.push(ActiveDialog::new(
|
||||
focus_handle,
|
||||
previous_focused_handle,
|
||||
builder,
|
||||
@@ -163,10 +163,10 @@ impl Root {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Close the topmost modal.
|
||||
pub fn close_modal(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
/// Close the topmost dialog.
|
||||
pub fn close_dialog(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some(handle) = self
|
||||
.active_modals
|
||||
.active_dialogs
|
||||
.pop()
|
||||
.and_then(|d| d.previous_focused_handle)
|
||||
.and_then(|h| h.upgrade())
|
||||
@@ -177,12 +177,12 @@ impl Root {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Close all modals.
|
||||
pub fn close_all_modals(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.active_modals.clear();
|
||||
/// Close all dialogs.
|
||||
pub fn close_all_dialogs(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.active_dialogs.clear();
|
||||
|
||||
let previous_focused_handle = self
|
||||
.active_modals
|
||||
.active_dialogs
|
||||
.first()
|
||||
.and_then(|d| d.previous_focused_handle.clone());
|
||||
|
||||
@@ -193,9 +193,9 @@ impl Root {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Check if there are any active modals.
|
||||
pub fn has_active_modals(&self) -> bool {
|
||||
!self.active_modals.is_empty()
|
||||
/// Check if there are any active dialogs.
|
||||
pub fn has_active_dialogs(&self) -> bool {
|
||||
!self.active_dialogs.is_empty()
|
||||
}
|
||||
|
||||
/// Push a notification to the notification layer.
|
||||
|
||||
+20
-20
@@ -3,24 +3,24 @@ use std::rc::Rc;
|
||||
use gpui::{App, ElementId, Entity, Window};
|
||||
|
||||
use crate::Root;
|
||||
use crate::modal::Modal;
|
||||
use crate::dialog::Dialog;
|
||||
use crate::notification::Notification;
|
||||
|
||||
/// Extension trait for [`Window`] to add modal, notification .. functionality.
|
||||
/// Extension trait for [`Window`] to add dialog, notification .. functionality.
|
||||
pub trait WindowExtension: Sized {
|
||||
/// Opens a Modal.
|
||||
fn open_modal<F>(&mut self, cx: &mut App, builder: F)
|
||||
/// Opens a Dialog.
|
||||
fn open_dialog<F>(&mut self, cx: &mut App, builder: F)
|
||||
where
|
||||
F: Fn(Modal, &mut Window, &mut App) -> Modal + 'static;
|
||||
F: Fn(Dialog, &mut Window, &mut App) -> Dialog + 'static;
|
||||
|
||||
/// Return true, if there is an active Modal.
|
||||
fn has_active_modal(&mut self, cx: &mut App) -> bool;
|
||||
/// Return true, if there is an active Dialog.
|
||||
fn has_active_dialog(&mut self, cx: &mut App) -> bool;
|
||||
|
||||
/// Closes the last active Modal.
|
||||
fn close_modal(&mut self, cx: &mut App);
|
||||
/// Closes the last active Dialog.
|
||||
fn close_dialog(&mut self, cx: &mut App);
|
||||
|
||||
/// Closes all active Modals.
|
||||
fn close_all_modals(&mut self, cx: &mut App);
|
||||
/// Closes all active Dialogs.
|
||||
fn close_all_dialogs(&mut self, cx: &mut App);
|
||||
|
||||
/// Returns number of notifications.
|
||||
fn notifications(&mut self, cx: &mut App) -> Rc<Vec<Entity<Notification>>>;
|
||||
@@ -46,31 +46,31 @@ pub trait WindowExtension: Sized {
|
||||
|
||||
impl WindowExtension for Window {
|
||||
#[inline]
|
||||
fn open_modal<F>(&mut self, cx: &mut App, builder: F)
|
||||
fn open_dialog<F>(&mut self, cx: &mut App, builder: F)
|
||||
where
|
||||
F: Fn(Modal, &mut Window, &mut App) -> Modal + 'static,
|
||||
F: Fn(Dialog, &mut Window, &mut App) -> Dialog + 'static,
|
||||
{
|
||||
Root::update(self, cx, move |root, window, cx| {
|
||||
root.open_modal(builder, window, cx);
|
||||
root.open_dialog(builder, window, cx);
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn has_active_modal(&mut self, cx: &mut App) -> bool {
|
||||
Root::read(self, cx).has_active_modals()
|
||||
fn has_active_dialog(&mut self, cx: &mut App) -> bool {
|
||||
Root::read(self, cx).has_active_dialogs()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn close_modal(&mut self, cx: &mut App) {
|
||||
fn close_dialog(&mut self, cx: &mut App) {
|
||||
Root::update(self, cx, move |root, window, cx| {
|
||||
root.close_modal(window, cx);
|
||||
root.close_dialog(window, cx);
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn close_all_modals(&mut self, cx: &mut App) {
|
||||
fn close_all_dialogs(&mut self, cx: &mut App) {
|
||||
Root::update(self, cx, |root, window, cx| {
|
||||
root.close_all_modals(window, cx);
|
||||
root.close_all_dialogs(window, cx);
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user