Skip to content

Commit

Permalink
Emoji picker/selector example (#420)
Browse files Browse the repository at this point in the history
![Xilem Emoji
Picker.](https://github.com/linebender/xilem/assets/36049421/4f9b418f-c9b3-4971-9711-587f21010e47)

Some notes:
1) The accessibility of this example probably isn't great. Not sure what
to do about this.
2) Our layout primitives aren't great; we use a grid, but ideally the
number of rows would be reactive to the available space.
3) The pagination is slightly hacked together - it should really try and
give you page numbers. I'm not planning to address this, unless someone
provides the algorithm

This was originally created to act as a screenshot for
linebender/linebender.github.io#56
  • Loading branch information
DJMcNab authored Jan 17, 2025
1 parent 320159b commit ecdd12b
Show file tree
Hide file tree
Showing 10 changed files with 579 additions and 227 deletions.
461 changes: 242 additions & 219 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ clippy.duplicated_attributes = "allow"
masonry = { version = "0.2.0", path = "masonry" }
xilem_core = { version = "0.1.0", path = "xilem_core" }
tree_arena = { version = "0.1.0", path = "tree_arena" }
vello = { git = "https://github.com/linebender/vello", rev = "a71236c7c8da10a6eaad4602267663339620835a" }
vello = { git = "https://github.com/linebender/vello", rev = "d5ebe12737861c521d7ce6ce02b800278367d07d" }
wgpu = "23.0.1"
kurbo = "0.11.1"
parley = { git = "https://github.com/linebender/parley", rev = "16b62518d467487ee15cb230fdb28530d89a8cf6", features = [
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ Some files used for examples are under different licenses:

- The font file (`RobotoFlex-Subset.ttf`) in `xilem/resources/fonts/roboto_flex/` is licensed solely as documented in that folder (and is not licensed under the Apache License, Version 2.0).
- The data file (`status.csv`) in `xilem/resources/data/http_cats_status/` is licensed solely as documented in that folder (and is not licensed under the Apache License, Version 2.0).
- The data file (`emoji.csv`) in `xilem/resources/data/emoji_names/` is licensed solely as documented in that folder (and is not licensed under the Apache License, Version 2.0).

## Contribution

Expand Down
8 changes: 2 additions & 6 deletions masonry/src/text/render_text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,7 @@ pub fn render_text(
let glyph_xform = synthesis
.skew()
.map(|angle| Affine::skew(angle.to_radians().tan() as f64, 0.0));
let coords = run
.normalized_coords()
.iter()
.map(|coord| vello::skrifa::instance::NormalizedCoord::from_bits(*coord))
.collect::<Vec<_>>();
let coords = run.normalized_coords();
let brush = &brushes[style.brush.0];
scene
.draw_glyphs(font)
Expand All @@ -81,7 +77,7 @@ pub fn render_text(
.transform(transform)
.glyph_transform(glyph_xform)
.font_size(font_size)
.normalized_coords(&coords)
.normalized_coords(coords)
.draw(
Fill::NonZero,
glyph_run.glyphs().map(|glyph| {
Expand Down
15 changes: 14 additions & 1 deletion xilem/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ license.workspace = true
repository.workspace = true
homepage.workspace = true
rust-version.workspace = true
exclude = ["/resources/fonts/roboto_flex/", "/resources/data/http_cats_status/"]
exclude = [
"/resources/fonts/roboto_flex/",
"/resources/data/http_cats_status/",
"/resources/data/emoji_names/",
]

[package.metadata.docs.rs]
all-features = true
Expand Down Expand Up @@ -78,6 +82,15 @@ path = "examples/variable_clock.rs"
# cdylib is required for cargo-apk
crate-type = ["cdylib"]

[[example]]
name = "emoji_picker"

[[example]]
name = "emoji_picker_android"
path = "examples/emoji_picker.rs"
# cdylib is required for cargo-apk
crate-type = ["cdylib"]

[lints]
workspace = true

Expand Down
1 change: 1 addition & 0 deletions xilem/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ Some files used for examples are under different licenses:

* The font file (`RobotoFlex-Subset.ttf`) in `resources/fonts/roboto_flex/` is licensed solely as documented in that folder (and is not licensed under the Apache License, Version 2.0).
* The data file (`status.csv`) in `resources/data/http_cats_status/` is licensed solely as documented in that folder (and is not licensed under the Apache License, Version 2.0).
* The data file (`emoji.csv`) in `resources/data/emoji_names/` is licensed solely as documented in that folder (and is not licensed under the Apache License, Version 2.0).

Note that these files are *not* distributed with the released crate.

Expand Down
188 changes: 188 additions & 0 deletions xilem/examples/emoji_picker.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
// Copyright 2024 the Xilem Authors
// SPDX-License-Identifier: Apache-2.0

//! A simple emoji picker.
#![expect(clippy::shadow_unrelated, reason = "Idiomatic for Xilem users")]

use winit::error::EventLoopError;
use xilem::{
core::map_state,
palette,
view::{button, flex, grid, label, prose, sized_box, Axis, FlexExt, FlexSpacer, GridExt},
Color, EventLoop, EventLoopBuilder, WidgetView, Xilem,
};

fn app_logic(data: &mut EmojiPagination) -> impl WidgetView<EmojiPagination> {
flex((
sized_box(flex(()).must_fill_major_axis(true)).height(50.), // Padding because of the info bar on Android
flex((
// TODO: Expose that this is a "zoom out" button accessibly
button("๐Ÿ”-", |data: &mut EmojiPagination| {
data.size = (data.size + 1).min(5);
}),
// TODO: Expose that this is a "zoom in" button accessibly
button("๐Ÿ”+", |data: &mut EmojiPagination| {
data.size = (data.size - 1).max(2);
}),
))
.direction(Axis::Horizontal),
picker(data).flex(1.0),
map_state(
paginate(
data.start_index,
(data.size * data.size) as usize,
data.emoji.len(),
),
|state: &mut EmojiPagination| &mut state.start_index,
),
data.last_selected
.map(|idx| label(format!("Selected: {}", data.emoji[idx].display)).text_size(40.)),
FlexSpacer::Fixed(10.),
))
.direction(Axis::Vertical)
.must_fill_major_axis(true)
}

fn picker(data: &mut EmojiPagination) -> impl WidgetView<EmojiPagination> {
let mut grid_items = vec![];
'outer: for y in 0..data.size as usize {
let row_idx = data.start_index + y * data.size as usize;
for x in 0..data.size as usize {
let idx = row_idx + x;
let emoji = data.emoji.get(idx);
let Some(emoji) = emoji else {
// There are no more emoji, no point still looping
break 'outer;
};
let view = flex((
// TODO: Expose that this button corresponds to the label below for accessibility?
sized_box(button(emoji.display, move |data: &mut EmojiPagination| {
data.last_selected = Some(idx);
}))
.expand_width(),
sized_box(
prose(emoji.name)
.alignment(xilem::TextAlignment::Middle)
.brush(if data.last_selected.is_some_and(|it| it == idx) {
// TODO: Ensure this selection indicator color is accessible
// TODO: Expose selected state to accessibility tree
palette::css::BLUE
} else {
Color::WHITE
}),
)
.expand_width(),
))
.must_fill_major_axis(true);
grid_items.push(view.grid_pos(x.try_into().unwrap(), y.try_into().unwrap()));
}
}

grid(
grid_items,
data.size.try_into().unwrap(),
data.size.try_into().unwrap(),
)
}

fn paginate(
current_start: usize,
count_per_page: usize,
max_count: usize,
) -> impl WidgetView<usize> {
let percentage = (current_start * 100) / max_count;

flex((
// TODO: Expose that this is a previous page button to accessibility
button("<-", move |data| {
*data = current_start.saturating_sub(count_per_page);
}),
label(format!("{percentage}%")),
button("->", move |data| {
let new_idx = current_start + count_per_page;
if new_idx < max_count {
*data = new_idx;
}
}),
))
.direction(Axis::Horizontal)
}

struct EmojiPagination {
size: u32,
last_selected: Option<usize>,
start_index: usize,
emoji: Vec<EmojiInfo>,
}

fn run(event_loop: EventLoopBuilder) -> Result<(), EventLoopError> {
let emoji = EmojiInfo::parse_file();
let data = EmojiPagination {
size: 4,
last_selected: None,
start_index: 0,
emoji,
};

let app = Xilem::new(data, app_logic);
app.run_windowed(event_loop, "First Example".into())
}

struct EmojiInfo {
name: &'static str,
display: &'static str,
}

impl EmojiInfo {
/// Parse the supported emoji's information.
fn parse_file() -> Vec<Self> {
let mut lines = EMOJI_NAMES_CSV.lines();
let first_line = lines.next();
assert_eq!(
first_line,
Some("display,name"),
"Probably wrong CSV-like file"
);
lines.flat_map(Self::parse_single).collect()
}

fn parse_single(line: &'static str) -> Option<Self> {
let (display, name) = line.split_once(',')?;
Some(Self { display, name })
}
}

/// A subset of emoji data from <https://github.com/iamcal/emoji-data>, used under the MIT license.
/// Full details can be found in `xilem/resources/data/emoji_names/README.md` from
/// the workspace root.
const EMOJI_NAMES_CSV: &str = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/resources/data/emoji_names/emoji.csv",
));

// Boilerplate code: Identical across all applications which support Android

#[expect(clippy::allow_attributes, reason = "No way to specify the condition")]
#[allow(dead_code, reason = "False positive: needed in not-_android version")]
// This is treated as dead code by the Android version of the example, but is actually live
// This hackery is required because Cargo doesn't care to support this use case, of one
// example which works across Android and desktop
fn main() -> Result<(), EventLoopError> {
run(EventLoop::with_user_event())
}
#[cfg(target_os = "android")]
// Safety: We are following `android_activity`'s docs here
#[expect(
unsafe_code,
reason = "We believe that there are no other declarations using this name in the compiled objects here"
)]
#[no_mangle]
fn android_main(app: winit::platform::android::activity::AndroidApp) {
use winit::platform::android::EventLoopBuilderExtAndroid;

let mut event_loop = EventLoop::with_user_event();
event_loop.with_android_app(app);

run(event_loop).expect("Can create app");
}
21 changes: 21 additions & 0 deletions xilem/resources/data/emoji_names/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
The MIT License (MIT)

Copyright (c) 2013 Cal Henderson

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
10 changes: 10 additions & 0 deletions xilem/resources/data/emoji_names/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Emoji names

The data in emoji.csv was adapted as a subset of <https://github.com/iamcal/emoji-data>.
These were extracted from <https://github.com/iamcal/emoji-data/blob/master/emoji_pretty.json> on 2024-09-03,
specifically as at commit [`a8174c74675355c8c6a9564516b2e961fe7257ef`](https://github.com/iamcal/emoji-data/blob/a8174c74675355c8c6a9564516b2e961fe7257ef/emoji_pretty.json).
Full license text can be found above in the source file.

## License

These are licensed solely under the MIT license, as found in [LICENSE](./LICENSE).
99 changes: 99 additions & 0 deletions xilem/resources/data/emoji_names/emoji.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
display,name
๐Ÿ˜,grinning face with smiling eyes
๐Ÿ˜‚,face with tears of joy
๐Ÿ˜ƒ,smiling face with open mouth
๐Ÿ˜„,smiling face with open mouth and smiling eyes
๐Ÿ˜…,smiling face with open mouth and cold sweat
๐Ÿ˜†,smiling face with open mouth and tightly-closed eyes
๐Ÿ˜‡,smiling face with halo
๐Ÿ˜ˆ,smiling face with horns
๐Ÿ˜‰,winking face
๐Ÿ˜Š,smiling face with smiling eyes
๐Ÿ˜‹,face savouring delicious food
๐Ÿ˜Œ,relieved face
๐Ÿ˜,smiling face with heart-shaped eyes
๐Ÿ˜Ž,smiling face with sunglasses
๐Ÿ˜,smirking face
๐Ÿ˜,neutral face
๐Ÿ˜‘,expressionless face
๐Ÿ˜’,unamused face
๐Ÿ˜“,face with cold sweat
๐Ÿ˜”,pensive face
๐Ÿ˜•,confused face
๐Ÿ˜–,confounded face
๐Ÿ˜—,kissing face
๐Ÿ˜˜,face throwing a kiss
๐Ÿ˜™,kissing face with smiling eyes
๐Ÿ˜š,kissing face with closed eyes
๐Ÿ˜›,face with stuck-out tongue
๐Ÿ˜œ,face with stuck-out tongue and winking eye
๐Ÿ˜,face with stuck-out tongue and tightly-closed eyes
๐Ÿ˜ž,disappointed face
๐Ÿ˜Ÿ,worried face
๐Ÿ˜ ,angry face
๐Ÿ˜ก,pouting face
๐Ÿ˜ข,crying face
๐Ÿ˜ฃ,persevering face
๐Ÿ˜ค,face with look of triumph
๐Ÿ˜ฅ,disappointed but relieved face
๐Ÿ˜ฆ,frowning face with open mouth
๐Ÿ˜ง,anguished face
๐Ÿ˜จ,fearful face
๐Ÿ˜ฉ,weary face
๐Ÿ˜ช,sleepy face
๐Ÿ˜ซ,tired face
๐Ÿ˜ฌ,grimacing face
๐Ÿ˜ญ,loudly crying face
๐Ÿ˜ฎโ€๐Ÿ’จ,face exhaling
๐Ÿ˜ฎ,face with open mouth
๐Ÿ˜ฏ,hushed face
๐Ÿ˜ฐ,face with open mouth and cold sweat
๐Ÿ˜ฑ,face screaming in fear
๐Ÿ˜ฒ,astonished face
๐Ÿ˜ณ,flushed face
๐Ÿ˜ด,sleeping face
๐Ÿ˜ตโ€๐Ÿ’ซ,face with spiral eyes
๐Ÿ˜ต,dizzy face
๐Ÿ˜ถโ€๐ŸŒซ๏ธ,face in clouds
๐Ÿ˜ถ,face without mouth
๐Ÿ˜ท,face with medical mask
๐Ÿ˜ธ,grinning cat face with smiling eyes
๐Ÿ˜น,cat face with tears of joy
๐Ÿ˜บ,smiling cat face with open mouth
๐Ÿ˜ป,smiling cat face with heart-shaped eyes
๐Ÿ˜ผ,cat face with wry smile
๐Ÿ˜ฝ,kissing cat face with closed eyes
๐Ÿ˜พ,pouting cat face
๐Ÿ˜ฟ,crying cat face
๐Ÿ™€,weary cat face
๐Ÿ™,slightly frowning face
๐Ÿ™‚โ€โ†”๏ธ,head shaking horizontally
๐Ÿ™‚โ€โ†•๏ธ,head shaking vertically
๐Ÿ™‚,slightly smiling face
๐Ÿ™ƒ,upside-down face
๐Ÿ™„,face with rolling eyes
๐Ÿ™…โ€โ™€๏ธ,woman gesturing no
๐Ÿ™…โ€โ™‚๏ธ,man gesturing no
๐Ÿ™…,face with no good gesture
๐Ÿ™†โ€โ™€๏ธ,woman gesturing ok
๐Ÿ™†โ€โ™‚๏ธ,man gesturing ok
๐Ÿ™†,face with ok gesture
๐Ÿ™‡โ€โ™€๏ธ,woman bowing
๐Ÿ™‡โ€โ™‚๏ธ,man bowing
๐Ÿ™‡,person bowing deeply
๐Ÿ™ˆ,see-no-evil monkey
๐Ÿ™‰,hear-no-evil monkey
๐Ÿ™Š,speak-no-evil monkey
๐Ÿ™‹โ€โ™€๏ธ,woman raising hand
๐Ÿ™‹โ€โ™‚๏ธ,man raising hand
๐Ÿ™‹,happy person raising one hand
๐Ÿ™Œ,person raising both hands in celebration
๐Ÿ™โ€โ™€๏ธ,woman frowning
๐Ÿ™โ€โ™‚๏ธ,man frowning
๐Ÿ™,person frowning
๐Ÿ™Žโ€โ™€๏ธ,woman pouting
๐Ÿ™Žโ€โ™‚๏ธ,man pouting
๐Ÿ™Ž,person with pouting face
๐Ÿ™,person with folded hands
๐Ÿš€,rocket
๐Ÿš,helicopter

0 comments on commit ecdd12b

Please sign in to comment.