Collab · CommentSidebar: threads are a list; each row is a Tab stop, named, selected, picked with Enter or Space
This commit is contained in:
3 files changed
+88
-7
No files matched your search
+39
-7
@@ -1,7 +1,7 @@
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
App, ElementId, Entity, InteractiveElement, IntoElement, ParentElement, RenderOnce,
|
App, ElementId, Entity, InteractiveElement, IntoElement, ParentElement, RenderOnce, Role,
|
||||||
SharedString, StatefulInteractiveElement, Styled, Window, div, prelude::*,
|
SharedString, StatefulInteractiveElement, Styled, Window, div, prelude::*,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ use crate::{
|
|||||||
forms::{OnValue, Run, TextInput},
|
forms::{OnValue, Run, TextInput},
|
||||||
i18n,
|
i18n,
|
||||||
primitives::IconName,
|
primitives::IconName,
|
||||||
theme::ControlSize,
|
theme::{ActiveTheme, ControlSize, Radius},
|
||||||
};
|
};
|
||||||
|
|
||||||
type OnThreadFlag = Rc<dyn Fn(&SharedString, bool, &mut Window, &mut App)>;
|
type OnThreadFlag = Rc<dyn Fn(&SharedString, bool, &mut Window, &mut App)>;
|
||||||
@@ -135,7 +135,15 @@ impl RenderOnce for CommentSidebar {
|
|||||||
i18n::text(cx, none, &[]),
|
i18n::text(cx, none, &[]),
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
let threads = shown.into_iter().map(|thread| {
|
let count = shown.len();
|
||||||
|
let theme = cx.theme();
|
||||||
|
let (ring, radius) = (theme.colors.focus, theme.radius(Radius::Md));
|
||||||
|
let threads = shown.into_iter().enumerate().map(|(at, thread)| {
|
||||||
|
let name: SharedString = match (thread.comments.first(), &thread.quote) {
|
||||||
|
(Some(first), _) => format!("{}: {}", first.author.name, first.body).into(),
|
||||||
|
(None, Some(quote)) => quote.clone(),
|
||||||
|
(None, None) => thread.key.clone(),
|
||||||
|
};
|
||||||
let key = thread.key.clone();
|
let key = thread.key.clone();
|
||||||
let active = self.active.as_ref() == Some(&key);
|
let active = self.active.as_ref() == Some(&key);
|
||||||
let mut item = CommentThread::new((self.id.clone(), format!("thread-{key}")), thread)
|
let mut item = CommentThread::new((self.id.clone(), format!("thread-{key}")), thread)
|
||||||
@@ -153,19 +161,43 @@ impl RenderOnce for CommentSidebar {
|
|||||||
item =
|
item =
|
||||||
item.on_react(move |ix, emoji, window, cx| react(&key, ix, emoji, window, cx));
|
item.on_react(move |ix, emoji, window, cx| react(&key, ix, emoji, window, cx));
|
||||||
}
|
}
|
||||||
let select = self.on_select.clone();
|
let select = self.on_select.clone().filter(|_| !active);
|
||||||
|
let row = key.clone();
|
||||||
div()
|
div()
|
||||||
.id((self.id.clone(), format!("pick-{key}")))
|
.id((self.id.clone(), format!("pick-{key}")))
|
||||||
.when_some(select.filter(|_| !active), |row, select| {
|
.debug_selector(move || format!("comment-{row}"))
|
||||||
|
.role(Role::ListItem)
|
||||||
|
.aria_label(name)
|
||||||
|
.aria_selected(active)
|
||||||
|
.aria_position_in_set(at + 1)
|
||||||
|
.aria_size_of_set(count)
|
||||||
|
.tab_index(0)
|
||||||
|
.rounded(radius)
|
||||||
|
.border_1()
|
||||||
|
.border_color(gpui::transparent_black())
|
||||||
|
.focus(move |style| style.border_color(ring))
|
||||||
|
.when_some(select, |row, select| {
|
||||||
|
let (click, press, pressed) = (select.clone(), key.clone(), key.clone());
|
||||||
row.on_click(move |_, window, cx| {
|
row.on_click(move |_, window, cx| {
|
||||||
log::info!("comment sidebar: {key}");
|
log::info!("comment sidebar: {press}");
|
||||||
select(&key, window, cx)
|
click(&press, window, cx)
|
||||||
|
})
|
||||||
|
.on_key_down(move |event, window, cx| {
|
||||||
|
let key = event.keystroke.key.as_str();
|
||||||
|
if matches!(key, "enter" | "space") && !event.keystroke.modifiers.modified()
|
||||||
|
{
|
||||||
|
cx.stop_propagation();
|
||||||
|
log::info!("comment sidebar: {pressed}");
|
||||||
|
select(&pressed, window, cx)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.child(item)
|
.child(item)
|
||||||
});
|
});
|
||||||
div()
|
div()
|
||||||
.id(self.id.clone())
|
.id(self.id.clone())
|
||||||
|
.role(Role::List)
|
||||||
|
.aria_label(i18n::text(cx, "comments.list", &[]))
|
||||||
.size_full()
|
.size_full()
|
||||||
.flex()
|
.flex()
|
||||||
.flex_col()
|
.flex_col()
|
||||||
|
|||||||
@@ -98,3 +98,50 @@ fn following_begun_from_outside_still_stops_on_escape(cx: &mut TestAppContext) {
|
|||||||
let back = cx.update(|window, cx| desk.read(cx).outside.focus_handle(cx).is_focused(window));
|
let back = cx.update(|window, cx| desk.read(cx).outside.focus_handle(cx).is_focused(window));
|
||||||
assert!(back, "focus returns to where it was");
|
assert!(back, "focus returns to where it was");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Two threads, one resolved, and the key each pick asked for.
|
||||||
|
struct Board {
|
||||||
|
picked: Vec<gpui::SharedString>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for Board {
|
||||||
|
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let thread = |key: &str, resolved| super::Thread {
|
||||||
|
key: key.to_string().into(),
|
||||||
|
quote: None,
|
||||||
|
comments: vec![super::Comment {
|
||||||
|
author: Peer::new("ada", "Ada", 0),
|
||||||
|
at: jiff::Timestamp::UNIX_EPOCH,
|
||||||
|
body: format!("About {key}").into(),
|
||||||
|
reactions: Vec::new(),
|
||||||
|
}],
|
||||||
|
resolved,
|
||||||
|
};
|
||||||
|
let pick = cx.entity();
|
||||||
|
div().size_full().child(
|
||||||
|
super::CommentSidebar::new("remarks", [thread("a", false), thread("b", true)])
|
||||||
|
.active(None, move |key, _, cx| {
|
||||||
|
pick.update(cx, |board, _| board.picked.push(key.clone()))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn without_resolving_one_list_takes_keyboard_picks(cx: &mut TestAppContext) {
|
||||||
|
cx.update(|cx| {
|
||||||
|
Theme::init(cx);
|
||||||
|
forms::bind_keys(cx);
|
||||||
|
});
|
||||||
|
let (board, cx) = cx.add_window_view(|_, _| Board { picked: Vec::new() });
|
||||||
|
cx.run_until_parked();
|
||||||
|
assert!(cx.debug_bounds("comment-a").is_some());
|
||||||
|
assert!(cx.debug_bounds("comment-b").is_some(), "resolved shows too");
|
||||||
|
cx.update(|window, cx| {
|
||||||
|
window.activate_window();
|
||||||
|
window.focus_next(cx);
|
||||||
|
});
|
||||||
|
cx.simulate_keystrokes("enter");
|
||||||
|
let picked = board.read_with(cx, |board, _| board.picked.clone());
|
||||||
|
assert_eq!(picked, vec![gpui::SharedString::from("a")]);
|
||||||
|
}
|
||||||
@@ -48,6 +48,7 @@ const EN: &[(&str, &str)] = &[
|
|||||||
("comments.none_open", "No open comments"),
|
("comments.none_open", "No open comments"),
|
||||||
("comments.none_resolved", "Nothing resolved yet"),
|
("comments.none_resolved", "Nothing resolved yet"),
|
||||||
("comments.none", "No comments"),
|
("comments.none", "No comments"),
|
||||||
|
("comments.list", "Comments"),
|
||||||
("time.now", "just now"),
|
("time.now", "just now"),
|
||||||
("time.ago", "{span} ago"),
|
("time.ago", "{span} ago"),
|
||||||
("time.in", "in {span}"),
|
("time.in", "in {span}"),
|
||||||
@@ -107,6 +108,7 @@ const ZH_CN: &[(&str, &str)] = &[
|
|||||||
("comments.none_open", "没有未解决的批注"),
|
("comments.none_open", "没有未解决的批注"),
|
||||||
("comments.none_resolved", "还没有已解决的批注"),
|
("comments.none_resolved", "还没有已解决的批注"),
|
||||||
("comments.none", "没有批注"),
|
("comments.none", "没有批注"),
|
||||||
|
("comments.list", "批注"),
|
||||||
("time.now", "刚刚"),
|
("time.now", "刚刚"),
|
||||||
("time.ago", "{span}前"),
|
("time.ago", "{span}前"),
|
||||||
("time.in", "{span}后"),
|
("time.in", "{span}后"),
|
||||||
|
|||||||
Reference in new issue
Block a user