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:
ZacharyZhang-NY committed 2026-09-26 05:10:37 -04:00
1 parent 0623855f38
commit 153cf95b99
34 files changed
+3297 -86

No files matched your search

+135
View File
@@ -0,0 +1,135 @@
use std::ops::Range;
use gpui::{
App, ContentMask, Entity, FontWeight, IntoElement, RenderOnce, SharedString, Styled, TextRun,
Window, canvas, fill, point, size,
};
use super::peers::Peer;
use crate::{
forms::TextInput,
theme::{ActiveTheme, Radius, TextSize},
typography::LEADING,
};
/// How strongly a peer's color washes their selection.
const WASH: f32 = 0.2;
/// Someone else's caret in a shared field: a bar in their color, their name on a flag above. Lay it over the field, in a box its size; it reads where `offset` sits as the field paints.
#[derive(IntoElement)]
pub struct RemoteCursor {
peer: Peer,
input: Entity<TextInput>,
offset: usize,
}
impl RemoteCursor {
pub fn new(peer: &Peer, input: &Entity<TextInput>, offset: usize) -> Self {
Self {
peer: peer.clone(),
input: input.clone(),
offset,
}
}
}
impl RenderOnce for RemoteCursor {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme();
let rem = window.rem_size();
let (color, ink) = (self.peer.color(cx), theme.colors.on_accent);
let bar = theme.caret_width().to_pixels(rem);
let text = theme.text_size(TextSize::Xs).to_pixels(rem);
let radius = theme.radius(Radius::Sm).to_pixels(rem);
let name: SharedString = self.peer.name.clone();
let (input, offset) = (self.input, self.offset);
canvas(
|_, _, _| {},
move |bounds, _, window, cx| {
let Some(caret) = input.read(cx).bounds_for(offset) else {
return;
};
if caret.bottom() <= bounds.top() || caret.top() >= bounds.bottom() {
return;
}
window.with_content_mask(Some(ContentMask { bounds }), |window| {
window.paint_quad(fill(
gpui::Bounds::new(caret.origin, size(bar, caret.size.height)),
color,
));
});
let font = gpui::Font {
weight: FontWeight::MEDIUM,
..window.text_style().font()
};
let run = TextRun {
len: name.len(),
font,
color: ink,
background_color: None,
underline: None,
strikethrough: None,
};
let line = window
.text_system()
.shape_line(name.clone(), text, &[run], None);
let (height, pad) = (text * LEADING, text / 2.0);
let flag = gpui::Bounds::new(
point(caret.left(), caret.top() - height),
size(line.width + pad * 2.0, height),
);
window.paint_quad(fill(flag, color).corner_radii(gpui::Corners {
top_left: radius,
top_right: radius,
bottom_right: radius,
bottom_left: gpui::Pixels::ZERO,
}));
if let Err(error) =
line.paint(point(flag.left() + pad, flag.top()), height, window, cx)
{
log::error!("remote cursor: name did not paint: {error:#}");
}
},
)
.absolute()
.inset_0()
}
}
/// Someone else's selection in a shared field, washed in their color. Lay it over the field, in a box its size; it reads where `range` sits as the field paints.
#[derive(IntoElement)]
pub struct RemoteSelection {
peer: Peer,
input: Entity<TextInput>,
range: Range<usize>,
}
impl RemoteSelection {
pub fn new(peer: &Peer, input: &Entity<TextInput>, range: Range<usize>) -> Self {
Self {
peer: peer.clone(),
input: input.clone(),
range,
}
}
}
impl RenderOnce for RemoteSelection {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let wash = self.peer.color(cx).opacity(WASH);
let (input, range) = (self.input, self.range);
canvas(
|_, _, _| {},
move |bounds, _, window, cx| {
let lines = input.read(cx).bounds_for_range(range.clone());
window.with_content_mask(Some(ContentMask { bounds }), |window| {
for line in lines {
window.paint_quad(fill(line, wash));
}
});
},
)
.absolute()
.inset_0()
}
}