From aa01d46e9fef0e191f0074974c89135e0be1141c Mon Sep 17 00:00:00 2001 From: Daniel McNab <36049421+DJMcNab@users.noreply.github.com> Date: Mon, 1 Jul 2024 15:03:46 +0100 Subject: [PATCH 1/2] Add a small emoji picker example Add license heading Add accessibility comments Fix clippy and formatting Fix android compilation --- .github/copyright.sh | 3 +- xilem/examples/emoji_picker.rs | 301 +++++++++++++++++++++++++++++++++ 2 files changed, 303 insertions(+), 1 deletion(-) create mode 100644 xilem/examples/emoji_picker.rs diff --git a/.github/copyright.sh b/.github/copyright.sh index c6e3522b9..a22254d65 100644 --- a/.github/copyright.sh +++ b/.github/copyright.sh @@ -7,7 +7,8 @@ # -g "!src/special_directory" # Check all the standard Rust source files -output=$(rg "^// Copyright (19|20)[\d]{2} (.+ and )?the Xilem Authors( and .+)?$\n^// SPDX-License-Identifier: Apache-2\.0$\n\n" --files-without-match --multiline -g "*.rs" .) +# Exclude the Emoji picker example because it also has some MIT licensed content +output=$(rg "^// Copyright (19|20)[\d]{2} (.+ and )?the Xilem Authors( and .+)?$\n^// SPDX-License-Identifier: Apache-2\.0$\n\n" --files-without-match --multiline -g "*.rs" -g "!xilem/examples/emoji_picker.rs" .) if [ -n "$output" ]; then echo -e "The following files lack the correct copyright header:\n" diff --git a/xilem/examples/emoji_picker.rs b/xilem/examples/emoji_picker.rs new file mode 100644 index 000000000..af848e0e5 --- /dev/null +++ b/xilem/examples/emoji_picker.rs @@ -0,0 +1,301 @@ +// Copyright 2024 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 AND MIT + +//! A simple emoji picker. +//! It is expected that the Emoji in this example may not render. +//! This is because Vello does not support any kinds of bitmap fonts. +//! +//! Note that the MIT license is needed because of the emoji data. +//! Everything except for the [`EMOJI`] constant is Apache 2.0 licensed. + +use xilem::{ + core::map_state, + view::{button, flex, label, prose, sized_box}, + AnyWidgetView, Axis, Color, EventLoop, EventLoopBuilder, WidgetView, Xilem, +}; + +fn app_logic(data: &mut EmojiPagination) -> impl WidgetView { + 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), + map_state( + paginate( + data.start_index, + (data.size * data.size) as usize, + EMOJI.len(), + ), + |state: &mut EmojiPagination| &mut state.start_index, + ), + data.last_selected + .map(|idx| label(format!("Selected: {}", EMOJI[idx].display)).text_size(40.)), + )) + .direction(Axis::Vertical) +} + +fn picker(data: &mut EmojiPagination) -> impl WidgetView { + let mut result = vec![]; + // TODO: We should be able to use a grid view here, but that isn't implemented + // We hack around it by making each item take up their proportion of the 400 + let dimensions = 400. / data.size as f64; + for y in 0..data.size as usize { + let mut row_contents = vec![]; + 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 = EMOJI.get(idx); + // TODO: Use OneOf2 + let view: Box> = match emoji { + Some(emoji) => { + let view = flex(( + // TODO: Expose that this button corresponds to the label below to 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 + Color::BLUE + } else { + Color::WHITE + }), + ) + .expand_width(), + )) + .must_fill_major_axis(true); + Box::new(view) + } + None => Box::new(flex(())), + }; + row_contents.push(sized_box(view).width(dimensions).height(dimensions)); + } + result.push(flex(row_contents).direction(Axis::Horizontal)); + } + + flex(result) +} + +fn paginate( + current_start: usize, + count_per_page: usize, + max_count: usize, +) -> impl WidgetView { + 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, + start_index: usize, +} + +fn run(event_loop: EventLoopBuilder) { + let data = EmojiPagination { + size: 4, + last_selected: None, + start_index: 0, + }; + + let app = Xilem::new(data, app_logic); + app.run_windowed(event_loop, "First Example".into()) + .unwrap(); +} + +struct EmojiInfo { + name: &'static str, + display: &'static str, +} + +const fn e(display: &'static str, name: &'static str) -> EmojiInfo { + EmojiInfo { name, display } +} + +// Data adapted from https://github.com/iamcal/emoji-data +// under the MIT License. Full license text included below this item +const EMOJI: &[EmojiInfo] = &[ + e("😁", "grinning face with smiling eyes"), + e("😂", "face with tears of joy"), + e("😃", "smiling face with open mouth"), + e("😄", "smiling face with open mouth and smiling eyes"), + e("😅", "smiling face with open mouth and cold sweat"), + e("😆", "smiling face with open mouth and tightly-closed eyes"), + e("😇", "smiling face with halo"), + e("😈", "smiling face with horns"), + e("😉", "winking face"), + e("😊", "smiling face with smiling eyes"), + e("😋", "face savouring delicious food"), + e("😌", "relieved face"), + e("😍", "smiling face with heart-shaped eyes"), + e("😎", "smiling face with sunglasses"), + e("😏", "smirking face"), + e("😐", "neutral face"), + e("😑", "expressionless face"), + e("😒", "unamused face"), + e("😓", "face with cold sweat"), + e("😔", "pensive face"), + e("😕", "confused face"), + e("😖", "confounded face"), + e("😗", "kissing face"), + e("😘", "face throwing a kiss"), + e("😙", "kissing face with smiling eyes"), + e("😚", "kissing face with closed eyes"), + e("😛", "face with stuck-out tongue"), + e("😜", "face with stuck-out tongue and winking eye"), + e("😝", "face with stuck-out tongue and tightly-closed eyes"), + e("😞", "disappointed face"), + e("😟", "worried face"), + e("😠", "angry face"), + e("😡", "pouting face"), + e("😢", "crying face"), + e("😣", "persevering face"), + e("😤", "face with look of triumph"), + e("😥", "disappointed but relieved face"), + e("😦", "frowning face with open mouth"), + e("😧", "anguished face"), + e("😨", "fearful face"), + e("😩", "weary face"), + e("😪", "sleepy face"), + e("😫", "tired face"), + e("😬", "grimacing face"), + e("😭", "loudly crying face"), + e("😮‍💨", "face exhaling"), + e("😮", "face with open mouth"), + e("😯", "hushed face"), + e("😰", "face with open mouth and cold sweat"), + e("😱", "face screaming in fear"), + e("😲", "astonished face"), + e("😳", "flushed face"), + e("😴", "sleeping face"), + e("😵‍💫", "face with spiral eyes"), + e("😵", "dizzy face"), + e("😶‍🌫️", "face in clouds"), + e("😶", "face without mouth"), + e("😷", "face with medical mask"), + e("😸", "grinning cat face with smiling eyes"), + e("😹", "cat face with tears of joy"), + e("😺", "smiling cat face with open mouth"), + e("😻", "smiling cat face with heart-shaped eyes"), + e("😼", "cat face with wry smile"), + e("😽", "kissing cat face with closed eyes"), + e("😾", "pouting cat face"), + e("😿", "crying cat face"), + e("🙀", "weary cat face"), + e("🙁", "slightly frowning face"), + e("🙂‍↔️", "head shaking horizontally"), + e("🙂‍↕️", "head shaking vertically"), + e("🙂", "slightly smiling face"), + e("🙃", "upside-down face"), + e("🙄", "face with rolling eyes"), + e("🙅‍♀️", "woman gesturing no"), + e("🙅‍♂️", "man gesturing no"), + e("🙅", "face with no good gesture"), + e("🙆‍♀️", "woman gesturing ok"), + e("🙆‍♂️", "man gesturing ok"), + e("🙆", "face with ok gesture"), + e("🙇‍♀️", "woman bowing"), + e("🙇‍♂️", "man bowing"), + e("🙇", "person bowing deeply"), + e("🙈", "see-no-evil monkey"), + e("🙉", "hear-no-evil monkey"), + e("🙊", "speak-no-evil monkey"), + e("🙋‍♀️", "woman raising hand"), + e("🙋‍♂️", "man raising hand"), + e("🙋", "happy person raising one hand"), + e("🙌", "person raising both hands in celebration"), + e("🙍‍♀️", "woman frowning"), + e("🙍‍♂️", "man frowning"), + e("🙍", "person frowning"), + e("🙎‍♀️", "woman pouting"), + e("🙎‍♂️", "man pouting"), + e("🙎", "person with pouting face"), + e("🙏", "person with folded hands"), + e("🚀", "rocket"), + e("🚁", "helicopter"), +]; + +// 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. + +#[cfg(not(target_os = "android"))] +#[allow(dead_code)] +// 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() { + run(EventLoop::with_user_event()); +} + +// Boilerplate code for android: Identical across all applications + +#[cfg(target_os = "android")] +use winit::platform::android::activity::AndroidApp; + +#[cfg(target_os = "android")] +// Safety: We are following `android_activity`'s docs here +// We believe that there are no other declarations using this name in the compiled objects here +#[allow(unsafe_code)] +#[no_mangle] +fn android_main(app: AndroidApp) { + use winit::platform::android::EventLoopBuilderExtAndroid; + + let mut event_loop = EventLoop::with_user_event(); + event_loop.with_android_app(app); + + run(event_loop); +} + +// TODO: This is a hack because of how we handle our examples in Cargo.toml +// Ideally, we change Cargo to be more sensible here? +#[cfg(target_os = "android")] +#[allow(dead_code)] +fn main() { + unreachable!() +} From 547c218116055aaa07be2148b63c482a6f9c04f4 Mon Sep 17 00:00:00 2001 From: Daniel McNab <36049421+DJMcNab@users.noreply.github.com> Date: Thu, 21 Nov 2024 16:00:49 +0000 Subject: [PATCH 2/2] Fixup for merge changes --- Cargo.lock | 16 ++++++------- xilem/Cargo.toml | 9 +++++++ xilem/examples/emoji_picker.rs | 44 ++++++++++++++-------------------- 3 files changed, 35 insertions(+), 34 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9c08eda57..b9ab528ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1538,9 +1538,9 @@ checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" [[package]] name = "hyper" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a" +checksum = "97818827ef4f364230e16705d4706e2897df2bb60617d6ca15d598025a3c481f" dependencies = [ "bytes", "futures-channel", @@ -2990,9 +2990,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.16" +version = "0.23.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eee87ff5d9b36712a58574e12e9f0ea80f915a5b0ac518d322b24a465617925e" +checksum = "7f1a745511c54ba6d4465e8d5dfbd81b45791756de28d4981af70d6dca128f1e" dependencies = [ "once_cell", "ring", @@ -3367,9 +3367,9 @@ dependencies = [ [[package]] name = "sync_wrapper" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" dependencies = [ "futures-core", ] @@ -4085,9 +4085,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.26.6" +version = "0.26.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841c67bff177718f1d4dfefde8d8f0e78f9b6589319ba88312f567fc5841a958" +checksum = "5d642ff16b7e79272ae451b7322067cdc17cadf68c23264be9d94a32319efe7e" dependencies = [ "rustls-pki-types", ] diff --git a/xilem/Cargo.toml b/xilem/Cargo.toml index 8acbb7a22..9231d5d1b 100644 --- a/xilem/Cargo.toml +++ b/xilem/Cargo.toml @@ -78,6 +78,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 diff --git a/xilem/examples/emoji_picker.rs b/xilem/examples/emoji_picker.rs index af848e0e5..1291fdbfa 100644 --- a/xilem/examples/emoji_picker.rs +++ b/xilem/examples/emoji_picker.rs @@ -8,10 +8,13 @@ //! Note that the MIT license is needed because of the emoji data. //! Everything except for the [`EMOJI`] constant is Apache 2.0 licensed. +#![expect(clippy::shadow_unrelated, reason = "Idiomatic for Xilem users")] + +use winit::error::EventLoopError; use xilem::{ core::map_state, - view::{button, flex, label, prose, sized_box}, - AnyWidgetView, Axis, Color, EventLoop, EventLoopBuilder, WidgetView, Xilem, + view::{button, flex, label, prose, sized_box, Axis}, + AnyWidgetView, Color, EventLoop, EventLoopBuilder, WidgetView, Xilem, }; fn app_logic(data: &mut EmojiPagination) -> impl WidgetView { @@ -118,7 +121,7 @@ struct EmojiPagination { start_index: usize, } -fn run(event_loop: EventLoopBuilder) { +fn run(event_loop: EventLoopBuilder) -> Result<(), EventLoopError> { let data = EmojiPagination { size: 4, last_selected: None, @@ -127,7 +130,6 @@ fn run(event_loop: EventLoopBuilder) { let app = Xilem::new(data, app_logic); app.run_windowed(event_loop, "First Example".into()) - .unwrap(); } struct EmojiInfo { @@ -264,38 +266,28 @@ const EMOJI: &[EmojiInfo] = &[ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -#[cfg(not(target_os = "android"))] -#[allow(dead_code)] +// 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() { - run(EventLoop::with_user_event()); +fn main() -> Result<(), EventLoopError> { + run(EventLoop::with_user_event()) } - -// Boilerplate code for android: Identical across all applications - -#[cfg(target_os = "android")] -use winit::platform::android::activity::AndroidApp; - #[cfg(target_os = "android")] // Safety: We are following `android_activity`'s docs here -// We believe that there are no other declarations using this name in the compiled objects here -#[allow(unsafe_code)] +#[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: AndroidApp) { +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); -} - -// TODO: This is a hack because of how we handle our examples in Cargo.toml -// Ideally, we change Cargo to be more sensible here? -#[cfg(target_os = "android")] -#[allow(dead_code)] -fn main() { - unreachable!() + run(event_loop).expect("Can create app"); }