T22: Collaboration
- presence: avatars ringed in each peer's color with where they are, press to follow; a breathing live dot; follow mode frames the view and stops on Escape - remote cursors and selections painted over a field from its own layout - comment threads with replies, reactions and resolve; a marker that opens a thread in a bubble; a sidebar of open and resolved threads - annotations washed in their authors' colors, notes on hover - suggestion mode: edits become suggestions word by word; track changes shows them struck and underlined, to accept or reject - sharing: invite by email with checked chips, roles, the access list and who the link lets in, in one dialog - primitives::hold_focus gives a mode its own focus, shared with ZenMode - AvatarGroup's tuck and count shared; Avatar::ring_color; TagInput::check; TextInput::bounds_for_range; Popover::size - icons: smile-plus, user-plus, message-square-diff - gallery: the Collaboration page with scripted states
This commit is contained in:
34 files changed
+3297
-86
No files matched your search
+45
-28
@@ -1,5 +1,5 @@
|
||||
use gpui::{
|
||||
AnyElement, App, ElementId, FontWeight, Hsla, ImageSource, IntoElement, ObjectFit,
|
||||
AnyElement, App, Div, ElementId, FontWeight, Hsla, ImageSource, IntoElement, ObjectFit,
|
||||
ParentElement, RenderOnce, SharedString, Styled, Window, div, prelude::*,
|
||||
};
|
||||
|
||||
@@ -57,6 +57,7 @@ pub struct Avatar {
|
||||
size: AvatarSize,
|
||||
square: bool,
|
||||
ring: bool,
|
||||
ring_color: Option<Hsla>,
|
||||
presence: Option<Presence>,
|
||||
}
|
||||
|
||||
@@ -71,6 +72,7 @@ impl Avatar {
|
||||
size: AvatarSize::default(),
|
||||
square: false,
|
||||
ring: false,
|
||||
ring_color: None,
|
||||
presence: None,
|
||||
}
|
||||
}
|
||||
@@ -109,6 +111,13 @@ impl Avatar {
|
||||
self
|
||||
}
|
||||
|
||||
/// A ring in `color`, as someone's own color marks them.
|
||||
pub fn ring_color(mut self, color: Hsla) -> Self {
|
||||
self.ring = true;
|
||||
self.ring_color = Some(color);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn presence(mut self, presence: Presence) -> Self {
|
||||
self.presence = Some(presence);
|
||||
self
|
||||
@@ -177,13 +186,44 @@ impl RenderOnce for Avatar {
|
||||
.text_color(tone)
|
||||
.text_size(theme.text_size(text))
|
||||
.font_weight(FontWeight::SEMIBOLD)
|
||||
.when(self.ring, |face| face.border_2().border_color(colors.bg))
|
||||
.when(self.ring, |face| {
|
||||
face.border_2()
|
||||
.border_color(self.ring_color.unwrap_or(colors.bg))
|
||||
})
|
||||
.child(face),
|
||||
)
|
||||
.children(dot)
|
||||
}
|
||||
}
|
||||
|
||||
/// An avatar's slot in a row, tucked under the one before unless `first`.
|
||||
pub(crate) fn tucked(first: bool, size: AvatarSize) -> Div {
|
||||
match (first, size) {
|
||||
(true, _) => div(),
|
||||
(false, AvatarSize::Xs | AvatarSize::Sm) => div().ml_neg_1(),
|
||||
(false, AvatarSize::Md) => div().ml_neg_2(),
|
||||
(false, AvatarSize::Lg | AvatarSize::Xl) => div().ml_neg_3(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The count that stands for `rest` people past the ones shown.
|
||||
pub(crate) fn more(rest: usize, size: AvatarSize, cx: &App) -> Div {
|
||||
let theme = cx.theme();
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.size(theme.avatar_size(size))
|
||||
.rounded_full()
|
||||
.border_2()
|
||||
.border_color(theme.colors.bg)
|
||||
.bg(theme.colors.hover)
|
||||
.text_size(theme.text_size(TextSize::Xs))
|
||||
.font_weight(FontWeight::MEDIUM)
|
||||
.text_color(theme.colors.fg_muted)
|
||||
.child(format!("+{rest}"))
|
||||
}
|
||||
|
||||
/// People side by side, each tucked under the next, and a count of the rest.
|
||||
#[derive(IntoElement)]
|
||||
pub struct AvatarGroup {
|
||||
@@ -216,39 +256,16 @@ impl AvatarGroup {
|
||||
|
||||
impl RenderOnce for AvatarGroup {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let theme = cx.theme();
|
||||
let rest = self.people.len().saturating_sub(self.max);
|
||||
let size = self.size;
|
||||
let tuck = |element: gpui::Div, first: bool| match (first, size) {
|
||||
(true, _) => element,
|
||||
(false, AvatarSize::Xs | AvatarSize::Sm) => element.ml_neg_1(),
|
||||
(false, AvatarSize::Md) => element.ml_neg_2(),
|
||||
(false, AvatarSize::Lg | AvatarSize::Xl) => element.ml_neg_3(),
|
||||
};
|
||||
let shown = self
|
||||
.people
|
||||
.into_iter()
|
||||
.take(self.max)
|
||||
.enumerate()
|
||||
.map(|(ix, person)| tuck(div(), ix == 0).child(person.size(size).ring()));
|
||||
let more = (rest > 0).then(|| {
|
||||
tuck(div(), false).child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.size(theme.avatar_size(size))
|
||||
.rounded_full()
|
||||
.border_2()
|
||||
.border_color(theme.colors.bg)
|
||||
.bg(theme.colors.hover)
|
||||
.text_size(theme.text_size(TextSize::Xs))
|
||||
.font_weight(FontWeight::MEDIUM)
|
||||
.text_color(theme.colors.fg_muted)
|
||||
.child(format!("+{rest}")),
|
||||
)
|
||||
});
|
||||
div().flex().items_center().children(shown).children(more)
|
||||
.map(|(ix, person)| tucked(ix == 0, size).child(person.size(size).ring()));
|
||||
let extra = (rest > 0).then(|| tucked(false, size).child(more(rest, size, cx)));
|
||||
div().flex().items_center().children(shown).children(extra)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ mod timeline;
|
||||
mod watermark;
|
||||
|
||||
pub use avatar::{Avatar, AvatarGroup, Presence, UserChip};
|
||||
pub(crate) use avatar::{more, tucked};
|
||||
pub use badge::{Badge, CountBadge, DotBadge, Tag, Tone};
|
||||
pub use before_after::BeforeAfter;
|
||||
pub use carousel::Carousel;
|
||||
|
||||
Reference in new issue
Block a user