Skip to content

Commit 36beee0

Browse files
tmmampagent
andauthored
feat(zbugs-gpui): react with any emoji (#162)
The reaction menu's More reactions… opens a picker with every emoji by group, or the ones a search finds by name or shortcode, in the user's skin tone, as zbugs' emoji picker. Enter picks the first one found. Co-authored-by: Amp <amp@ampcode.com>
1 parent 52d0b63 commit 36beee0

5 files changed

Lines changed: 252 additions & 1 deletion

File tree

‎apps/zbugs-gpui/Cargo.lock‎

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎apps/zbugs-gpui/Cargo.toml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ publish = false
1414
anyhow = "1"
1515
# Relative times in the local time zone, as zbugs' `RelativeTime`.
1616
chrono = { version = "0.4", default-features = false, features = ["clock"] }
17+
# The reaction picker's emoji, names, and skin tones, as zbugs' emoji picker.
18+
emojis = "0.9"
1719
futures = "0.3"
1820
# The random path the GitHub login's loopback redirect comes back to.
1921
getrandom = "0.3"

‎apps/zbugs-gpui/src/app.rs‎

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#[cfg(feature = "e2e")]
55
pub mod e2e;
66

7+
use crate::emoji_picker::{EmojiPicker, OnPick as OnPickEmoji};
78
use crate::live::Live;
89
use crate::picker::{self, Choice};
910
use crate::session::{Session, app_dir, now_ms, save_setting};
@@ -1366,7 +1367,6 @@ impl ZbugsApp {
13661367
self.mutate(request, cx);
13671368
}
13681369

1369-
/// Takes back the user's reaction with `unicode`, or adds it.
13701370
/// The user's skin tone for reactions, 0 for none.
13711371
fn skin_tone(&self) -> u32 {
13721372
let pref = self
@@ -1386,6 +1386,7 @@ impl ZbugsApp {
13861386
self.mutate(SET_USER_PREF.with(&args), cx);
13871387
}
13881388

1389+
/// Takes back the user's reaction with `unicode`, or adds it.
13891390
fn toggle_emoji(
13901391
&mut self,
13911392
subject: Subject,
@@ -1416,6 +1417,7 @@ impl ZbugsApp {
14161417
.find(|e| normalize_emoji(&e.value) == key)
14171418
.map(|e| e.annotation.clone())
14181419
})
1420+
.or_else(|| emojis::get(&key).map(|e| e.name().to_owned()))
14191421
.unwrap_or_default();
14201422
let args = AddEmojiArgs {
14211423
id: make_id(),
@@ -1431,6 +1433,39 @@ impl ZbugsApp {
14311433
self.mutate(add.with(&args), cx);
14321434
}
14331435

1436+
/// Opens the picker with every emoji, to react with any of them.
1437+
fn open_emoji_picker(
1438+
&mut self,
1439+
subject: Subject,
1440+
subject_id: &str,
1441+
emoji: &Rc<Vec<Emoji>>,
1442+
window: &mut Window,
1443+
cx: &mut Context<Self>,
1444+
) {
1445+
let (app, all, id) = (
1446+
cx.entity().downgrade(),
1447+
emoji.clone(),
1448+
subject_id.to_owned(),
1449+
);
1450+
let on_pick: OnPickEmoji = Rc::new(move |unicode, _, window, cx| {
1451+
window.close_dialog(cx);
1452+
let _ = app.update(cx, |this, cx| {
1453+
this.toggle_emoji(subject, &id, &all, unicode, cx)
1454+
});
1455+
});
1456+
let picker = cx.new(|cx| EmojiPicker::new(self.skin_tone(), on_pick, window, cx));
1457+
let query = picker.read(cx).query.clone();
1458+
window.open_dialog(cx, move |dialog, _, _| {
1459+
dialog
1460+
.w(px(400.))
1461+
.title("Add reaction")
1462+
.child(picker.clone())
1463+
});
1464+
window.defer(cx, move |window, cx| {
1465+
query.update(cx, |state, cx| state.focus(window, cx));
1466+
});
1467+
}
1468+
14341469
fn post_comment(&mut self, window: &mut Window, cx: &mut Context<Self>) {
14351470
let Some(issue_id) = self
14361471
.selected
@@ -2175,6 +2210,14 @@ impl ZbugsApp {
21752210
}),
21762211
);
21772212
}
2213+
let (picker, all, id) = (app.clone(), all.clone(), id.clone());
2214+
menu = menu.item(PopupMenuItem::new("More reactions…").on_click(
2215+
move |_, window, cx| {
2216+
let _ = picker.update(cx, |this, cx| {
2217+
this.open_emoji_picker(subject, &id, &all, window, cx)
2218+
});
2219+
},
2220+
));
21782221
let app = app.clone();
21792222
menu.separator()
21802223
.submenu("Skin tone", window, cx, move |mut menu, _, _| {
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
//! The reaction picker: every emoji by group, or the ones a search finds,
2+
//! in the user's skin tone, as zbugs' emoji picker.
3+
4+
use gpui_kit::component::input::{Input, InputEvent, InputState};
5+
use gpui_kit::component::tooltip::Tooltip;
6+
use gpui_kit::component::*;
7+
use gpui_kit::*;
8+
use std::rc::Rc;
9+
10+
const COLUMNS: usize = 9;
11+
const CELL: f32 = 36.;
12+
13+
/// What the picker does with the emoji the user picks, and its name.
14+
pub type OnPick = Rc<dyn Fn(&str, &str, &mut Window, &mut App)>;
15+
16+
enum Row {
17+
Group(&'static str),
18+
Emoji(Vec<&'static emojis::Emoji>),
19+
}
20+
21+
pub struct EmojiPicker {
22+
pub query: Entity<InputState>,
23+
tone: Option<emojis::SkinTone>,
24+
rows: Rc<Vec<Row>>,
25+
on_pick: OnPick,
26+
scroll: UniformListScrollHandle,
27+
_search: Subscription,
28+
}
29+
30+
impl EmojiPicker {
31+
/// `tone` as the `emojiSkinTone` pref keeps it: 0 for none, 1 to 5.
32+
pub fn new(tone: u32, on_pick: OnPick, window: &mut Window, cx: &mut Context<Self>) -> Self {
33+
use emojis::SkinTone::*;
34+
let tone = [Light, MediumLight, Medium, MediumDark, Dark]
35+
.get((tone as usize).wrapping_sub(1))
36+
.copied();
37+
let query = cx.new(|cx| InputState::new(window, cx).placeholder("Search emoji"));
38+
let search = cx.subscribe_in(&query, window, |this, query, event, window, cx| {
39+
match event {
40+
InputEvent::Change => {
41+
this.rows = Rc::new(rows(&query.read(cx).value()));
42+
this.scroll.scroll_to_item(0, ScrollStrategy::Top);
43+
cx.notify();
44+
}
45+
// Enter picks the first emoji the search found.
46+
InputEvent::PressEnter { .. } => {
47+
let first = this.rows.iter().find_map(|row| match row {
48+
Row::Emoji(row) => row.first().copied(),
49+
Row::Group(_) => None,
50+
});
51+
if let Some(emoji) = first {
52+
this.pick(emoji, window, cx);
53+
}
54+
}
55+
_ => {}
56+
}
57+
});
58+
Self {
59+
query,
60+
tone,
61+
rows: Rc::new(rows("")),
62+
on_pick,
63+
scroll: UniformListScrollHandle::new(),
64+
_search: search,
65+
}
66+
}
67+
68+
fn toned(&self, emoji: &'static emojis::Emoji) -> &'static emojis::Emoji {
69+
self.tone
70+
.and_then(|tone| emoji.with_skin_tone(tone))
71+
.unwrap_or(emoji)
72+
}
73+
74+
fn pick(&self, emoji: &'static emojis::Emoji, window: &mut Window, cx: &mut App) {
75+
(self.on_pick)(self.toned(emoji).as_str(), emoji.name(), window, cx);
76+
}
77+
}
78+
79+
/// The groups of every emoji, or, for a search, the emoji with a word in
80+
/// their name or shortcodes that starts with each word of `query`.
81+
fn rows(query: &str) -> Vec<Row> {
82+
// The emoji macOS 14 draws; Apple's font lags newer Unicode releases.
83+
let drawn = |e: &&emojis::Emoji| e.unicode_version() <= emojis::UnicodeVersion::new(15, 0);
84+
let query = query.trim().to_lowercase();
85+
let chunks = |found: Vec<&'static emojis::Emoji>| {
86+
found
87+
.chunks(COLUMNS)
88+
.map(|row| Row::Emoji(row.to_vec()))
89+
.collect::<Vec<_>>()
90+
};
91+
if query.is_empty() {
92+
return emojis::Group::iter()
93+
.flat_map(|group| {
94+
let found = group.emojis().filter(drawn).collect();
95+
std::iter::once(Row::Group(group_name(group))).chain(chunks(found))
96+
})
97+
.collect();
98+
}
99+
let found = emojis::iter()
100+
.filter(drawn)
101+
.filter(|e| {
102+
let words = e
103+
.name()
104+
.split([' ', '-', ':', ','])
105+
.chain(e.shortcodes().flat_map(|s| s.split(['_', '-'])))
106+
.map(str::to_lowercase)
107+
.collect::<Vec<_>>();
108+
query
109+
.split_whitespace()
110+
.all(|q| words.iter().any(|w| w.starts_with(q)))
111+
})
112+
.collect();
113+
chunks(found)
114+
}
115+
116+
fn group_name(group: emojis::Group) -> &'static str {
117+
use emojis::Group::*;
118+
match group {
119+
SmileysAndEmotion => "Smileys and emotion",
120+
PeopleAndBody => "People and body",
121+
AnimalsAndNature => "Animals and nature",
122+
FoodAndDrink => "Food and drink",
123+
TravelAndPlaces => "Travel and places",
124+
Activities => "Activities",
125+
Objects => "Objects",
126+
Symbols => "Symbols",
127+
Flags => "Flags",
128+
}
129+
}
130+
131+
impl Render for EmojiPicker {
132+
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
133+
let rows = self.rows.clone();
134+
let list = if rows.is_empty() {
135+
div()
136+
.h(px(CELL * 8.))
137+
.p_2()
138+
.text_color(cx.theme().muted_foreground)
139+
.child("No emoji found")
140+
.into_any_element()
141+
} else {
142+
uniform_list(
143+
"emoji",
144+
rows.len(),
145+
cx.processor(move |this, range: std::ops::Range<usize>, _, cx| {
146+
range
147+
.map(|ix| match &rows[ix] {
148+
Row::Group(name) => div()
149+
.h(px(CELL))
150+
.flex()
151+
.items_end()
152+
.pb_1()
153+
.text_xs()
154+
.text_color(cx.theme().muted_foreground)
155+
.child(*name)
156+
.into_any_element(),
157+
Row::Emoji(row) => h_flex()
158+
.h(px(CELL))
159+
.children(row.iter().map(|&emoji| {
160+
let shown = this.toned(emoji);
161+
let name = SharedString::from(emoji.name());
162+
div()
163+
.id(SharedString::from(format!("emoji-{}", emoji.as_str())))
164+
.size(px(CELL))
165+
.flex()
166+
.items_center()
167+
.justify_center()
168+
.rounded_md()
169+
.text_xl()
170+
.cursor_pointer()
171+
.hover(|s| s.bg(cx.theme().accent))
172+
.child(shown.as_str())
173+
.tooltip(move |window, cx| {
174+
Tooltip::new(name.clone()).build(window, cx)
175+
})
176+
.on_click(cx.listener(move |this, _, window, cx| {
177+
this.pick(emoji, window, cx)
178+
}))
179+
}))
180+
.into_any_element(),
181+
})
182+
.collect::<Vec<_>>()
183+
}),
184+
)
185+
.track_scroll(&self.scroll)
186+
.h(px(CELL * 8.))
187+
.into_any_element()
188+
};
189+
v_flex()
190+
.gap_2()
191+
.w(px(CELL * COLUMNS as f32))
192+
.child(Input::new(&self.query))
193+
.child(list)
194+
}
195+
}

‎apps/zbugs-gpui/src/main.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! zbugs on the Zero Rust client, in GPUI.
22
33
mod app;
4+
mod emoji_picker;
45
mod http;
56
mod live;
67
mod login;

0 commit comments

Comments
 (0)