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
@@ -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()
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user