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
+3
-1
@@ -79,7 +79,9 @@ pub(crate) use mention::{Suggestions, active_trigger, handles_matching};
|
||||
pub use multi::MultiSelect;
|
||||
pub use number::{NumberInput, ScrubInput};
|
||||
pub use options::Choice;
|
||||
pub(crate) use options::{OnFlag, OnValue, Pick, Run, float, float_height, step, surface};
|
||||
pub(crate) use options::{
|
||||
OnFlag, OnValue, OnValues, Pick, Run, float, float_height, step, surface,
|
||||
};
|
||||
pub use pairs::{FieldArray, KeyValueInput, ListInput};
|
||||
pub use path::PathInput;
|
||||
pub use pattern::{RegexInput, regex_highlights};
|
||||
|
||||
+14
-5
@@ -7,7 +7,7 @@ use gpui::{
|
||||
|
||||
use super::{InputEvent, TextInput, text::Backspace};
|
||||
use crate::{
|
||||
data_display::Tag,
|
||||
data_display::{Tag, Tone},
|
||||
theme::{ActiveTheme, ControlSize, Radius, TextSize},
|
||||
};
|
||||
|
||||
@@ -50,6 +50,7 @@ pub struct TagInput {
|
||||
id: ElementId,
|
||||
tags: Vec<SharedString>,
|
||||
placeholder: SharedString,
|
||||
check: Option<fn(&str) -> bool>,
|
||||
on_change: Option<OnChange>,
|
||||
}
|
||||
|
||||
@@ -62,6 +63,7 @@ impl TagInput {
|
||||
id: id.into(),
|
||||
tags: tags.into_iter().map(Into::into).collect(),
|
||||
placeholder: "Add a tag".into(),
|
||||
check: None,
|
||||
on_change: None,
|
||||
}
|
||||
}
|
||||
@@ -71,6 +73,12 @@ impl TagInput {
|
||||
self
|
||||
}
|
||||
|
||||
/// Marks each tag that fails `check` as danger, as text that is not an address.
|
||||
pub fn check(mut self, check: fn(&str) -> bool) -> Self {
|
||||
self.check = Some(check);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_change(
|
||||
mut self,
|
||||
handler: impl Fn(Vec<SharedString>, &mut Window, &mut App) + 'static,
|
||||
@@ -112,16 +120,17 @@ impl RenderOnce for TagInput {
|
||||
let owner = self.id.clone();
|
||||
let chips = self.tags.iter().enumerate().map(|(ix, tag)| {
|
||||
let (tags, on_change) = (self.tags.clone(), self.on_change.clone());
|
||||
Tag::new((owner.clone(), format!("tag-{ix}")), tag.clone()).on_remove(
|
||||
move |window, cx| {
|
||||
let failed = self.check.is_some_and(|check| !check(tag));
|
||||
Tag::new((owner.clone(), format!("tag-{ix}")), tag.clone())
|
||||
.when(failed, |tag| tag.tone(Tone::Danger))
|
||||
.on_remove(move |window, cx| {
|
||||
let mut next = tags.clone();
|
||||
next.remove(ix);
|
||||
log::info!("tag input: removed one, {} left", next.len());
|
||||
if let Some(on_change) = &on_change {
|
||||
on_change(next, window, cx);
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
});
|
||||
let (tags, on_change, empty_check) =
|
||||
(self.tags.clone(), self.on_change.clone(), input.clone());
|
||||
|
||||
+21
-1
@@ -7,7 +7,7 @@ mod upload;
|
||||
|
||||
use gpui::{
|
||||
AppContext as _, Context, Entity, EntityInputHandler, IntoElement, KeyUpEvent, Keystroke,
|
||||
ParentElement, Render, TestAppContext, VisualTestContext, Window, div,
|
||||
ParentElement, Render, TestAppContext, VisualTestContext, Window, div, px,
|
||||
};
|
||||
|
||||
use super::{Input, MaskedInput, NumberInput, PinInput, TextInput, bind_keys};
|
||||
@@ -279,3 +279,23 @@ fn a_composition_inside_a_masked_field_fits_from_where_it_began(cx: &mut TestApp
|
||||
});
|
||||
assert_eq!(text(&state, cx), "(192) 345");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn a_range_gives_a_box_per_line_it_spans(cx: &mut TestAppContext) {
|
||||
let (fields, cx) = open_with(1, false, |input| input.multi_line(2, 4), cx);
|
||||
fields[0].update(cx, |input, cx| input.set_text("lift\ntone", cx));
|
||||
cx.run_until_parked();
|
||||
cx.update(|window, _| window.refresh());
|
||||
cx.run_until_parked();
|
||||
let (one, two) = fields[0].read_with(cx, |input, _| {
|
||||
(input.bounds_for_range(1..3), input.bounds_for_range(2..7))
|
||||
});
|
||||
assert_eq!(one.len(), 1);
|
||||
assert!(one[0].size.width > px(0.0));
|
||||
assert_eq!(two.len(), 2, "{two:?}");
|
||||
assert!(two[1].top() > two[0].top());
|
||||
assert!(
|
||||
two[1].left() < two[0].left(),
|
||||
"the second line starts at the edge"
|
||||
);
|
||||
}
|
||||
@@ -358,7 +358,7 @@ fn keep_in_view(
|
||||
}
|
||||
|
||||
/// Boxes covering display offsets `start..end`, relative to the text origin.
|
||||
fn selection_rects(
|
||||
pub(super) fn selection_rects(
|
||||
lines: &[(usize, WrappedLine)],
|
||||
start: usize,
|
||||
end: usize,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use std::ops::Range;
|
||||
|
||||
use gpui::{Bounds, Pixels, Point};
|
||||
|
||||
use super::TextInput;
|
||||
use super::{TextInput, element::selection_rects};
|
||||
|
||||
impl TextInput {
|
||||
/// The caret's box in window coordinates, once laid out.
|
||||
@@ -22,6 +24,34 @@ impl TextInput {
|
||||
))
|
||||
}
|
||||
|
||||
/// A range's boxes in the window, a line each, from the last layout; later offsets clamp to its end.
|
||||
pub fn bounds_for_range(&self, range: Range<usize>) -> Vec<Bounds<Pixels>> {
|
||||
let Some(layout) = self.layout.as_ref() else {
|
||||
return Vec::new();
|
||||
};
|
||||
if layout.placeholder {
|
||||
return Vec::new();
|
||||
}
|
||||
let laid = layout
|
||||
.lines
|
||||
.last()
|
||||
.map_or(0, |(start, line)| start + line.len());
|
||||
let (start, end) = (
|
||||
self.display_offset(range.start).min(laid),
|
||||
self.display_offset(range.end).min(laid),
|
||||
);
|
||||
selection_rects(
|
||||
&layout.lines,
|
||||
start,
|
||||
end,
|
||||
layout.line_height,
|
||||
layout.bounds.size.width,
|
||||
)
|
||||
.into_iter()
|
||||
.map(|rect| Bounds::new(rect.origin + layout.bounds.origin - self.scroll, rect.size))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Top-left of a display offset, relative to the text's origin.
|
||||
pub(crate) fn position_for(&self, display: usize) -> Option<Point<Pixels>> {
|
||||
let layout = self.layout.as_ref()?;
|
||||
|
||||
Reference in new issue
Block a user