├── .gitignore ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── UNLICENSE ├── examples └── demo.rs ├── media └── demo.png └── src ├── exposed.rs └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /.vscode 3 | /Cargo.lock 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "egui-twemoji" 3 | description = "An egui widget that renders colored Twemojis." 4 | license = "Unlicense OR MIT OR Apache-2.0" 5 | exclude = ["media/**"] 6 | documentation = "https://docs.rs/egui-twemoji" 7 | homepage = "https://github.com/zeozeozeo/egui-twemoji" 8 | repository = "https://github.com/zeozeozeo/egui-twemoji" 9 | categories = ["gui"] 10 | keywords = ["egui", "emoji", "twemoji", "widget"] 11 | readme = "README.md" 12 | version = "0.7.0" 13 | edition = "2021" 14 | 15 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 16 | 17 | [dependencies] 18 | egui = { version = "0.31.1", default-features = false } 19 | twemoji-assets = { version = "1.3.0", default-features = false } 20 | unicode-segmentation = "1.12.0" 21 | 22 | [dev-dependencies] 23 | eframe = "0.31.1" 24 | egui_extras = { version = "0.31.1", features = ["svg"] } 25 | 26 | [features] 27 | default = ["svg"] 28 | 29 | ## SVG emojis 30 | svg = ["twemoji-assets/svg"] 31 | 32 | ## PNG emojis 33 | png = ["twemoji-assets/png"] 34 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2024 zeozeozeo 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # egui-twemoji 2 | 3 | ![docs.rs](https://img.shields.io/docsrs/egui-twemoji) ![Downloads on Crates.io](https://img.shields.io/crates/d/egui-twemoji) 4 | 5 | An [egui](https://egui.rs/) widget that renders colored [Twemojis](https://github.com/twitter/twemoji). Based on [twemoji-assets](https://github.com/cptpiepmatz/twemoji-assets). 6 | 7 | ![demo](/media/demo.png) 8 | 9 | # How to use 10 | 11 | Make sure you've installed `egui_extras` image loaders (required for rendering SVG and PNG emotes): 12 | 13 | ```rust 14 | // don't do this every frame - only when the app is created! 15 | egui_extras::install_image_loaders(&cc.egui_ctx); 16 | ``` 17 | 18 | And then: 19 | 20 | ```rust 21 | use egui_twemoji::EmojiLabel; 22 | 23 | fn show_label(ui: &mut egui::Ui) { 24 | EmojiLabel::new("⭐ egui-twemoji 🐦✨").show(ui); 25 | } 26 | ``` 27 | 28 | For a more sophisticated example, see the `demo` example (`cargo run --example demo`) 29 | 30 | `EmojiLabel` supports all functions that a normal 31 | [Label](https://docs.rs/egui/latest/egui/widgets/struct.Label.html) does. 32 | 33 | # Features 34 | 35 | * `svg`: use SVG emoji assets (`egui_extras/svg` is required) 36 | * `png`: use PNG emoji assets (`egui_extras/image` is required) 37 | 38 | By default, the `svg` feature is activated. 39 | 40 | # License 41 | 42 | Unlicense OR MIT OR Apache-2.0 43 | -------------------------------------------------------------------------------- /UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /examples/demo.rs: -------------------------------------------------------------------------------- 1 | use egui::RichText; 2 | use egui_twemoji::EmojiLabel; 3 | 4 | fn main() -> Result<(), eframe::Error> { 5 | let options = eframe::NativeOptions { 6 | viewport: egui::ViewportBuilder::default().with_inner_size([320.0, 240.0]), 7 | ..Default::default() 8 | }; 9 | eframe::run_native( 10 | "egui-twemoji demo", 11 | options, 12 | Box::new(|cc| { 13 | // this is important: we are going to be rendering the emojis as SVGs 14 | egui_extras::install_image_loaders(&cc.egui_ctx); 15 | 16 | Ok(Box::::default()) 17 | }), 18 | ) 19 | } 20 | 21 | #[derive(Default)] 22 | struct ExampleApp { 23 | paste_field: String, 24 | } 25 | 26 | impl eframe::App for ExampleApp { 27 | fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { 28 | egui::CentralPanel::default().show(ctx, |ui| { 29 | egui::ScrollArea::vertical().show(ui, |ui| { 30 | EmojiLabel::new("⭐ egui-twemoji 🐦 demo ✨").show(ui); 31 | if EmojiLabel::new( 32 | RichText::new("👉 This 👈 is a strong 💪😈 RichText 🤑💰 label").strong(), 33 | ) 34 | .show(ui) 35 | .hovered() 36 | { 37 | EmojiLabel::new("hovered! 😸").show(ui); 38 | } 39 | EmojiLabel::new("Yes 👍, you 🤟 can 🎥 select 📝 and copy 🍝 this 👌").show(ui); 40 | 41 | ui.separator(); 42 | EmojiLabel::new("Paste 🆒 text here 📝📜:").show(ui); 43 | ui.text_edit_multiline(&mut self.paste_field); 44 | 45 | ui.collapsing("Emoji Madness (laggy)", |ui| { 46 | emoji_madness(ui); 47 | }); 48 | }); 49 | }); 50 | } 51 | } 52 | 53 | fn emoji_madness(ui: &mut egui::Ui) { 54 | EmojiLabel::new("🙅🙆🙇🙋🙌🙍🙎🙏✂✈✉✊✋✌✏❄❤🚀🚃🚄🚅").show(ui); 55 | EmojiLabel::new("🚇🚉🚌🚏🚑🚒🚓🚕🚗🚙🚚🚢🚤🚥🚧🚨🚩🚪🚫").show(ui); 56 | EmojiLabel::new("🚬🚲🚶🚽🛀⌚⌛⏰⏳☁☎☔☕♨♻♿⚓⚡⚽⚾⛄⛅").show(ui); 57 | EmojiLabel::new("⛪⛲⛳⛵⛺⭐⛽🌀🌁🌂🌃🌄🌅🌆🌇🌈🌉🌊🌋").show(ui); 58 | EmojiLabel::new("🌏🌙🌛🌟🌠🌰🌱🌴🌵🌷🌸🌹🌺🌻🌼🌽🌾🌿🍀🍁").show(ui); 59 | EmojiLabel::new("🍂🍃🍄🍅🍆🍇🍈🍉🍊🍌🍍🍎🍏🍑🍒🍓🍔🍕🍖").show(ui); 60 | EmojiLabel::new("🍗🍘🍙🍚🍛🍜🍝🍞🍟🍠🍡🍢🍣🍤🍥🍦🍧🍨🍩").show(ui); 61 | EmojiLabel::new("🍪🍫🍬🍭🍮🍯🍰🍱🍲🍳🍴🍵🍶🍷🍸🍹🍺🍻🎀🎁").show(ui); 62 | EmojiLabel::new("🎂🎃🎄🎅🎆🎇🎈🎉🎊🎋🎌🎍🎎🎏🎐🎑🎒🎓🎠").show(ui); 63 | EmojiLabel::new("🎡🎢🎣🎤🎥🎦🎧🎨🎩🎪🎫🎬🎭🎮🎯🎰🎱🎲").show(ui); 64 | EmojiLabel::new("🎳🎴🎵🎶🎷🎸🎹🎺🎻🎽🎾🎿🏀🏁🏂🏃🏄🏆").show(ui); 65 | EmojiLabel::new("🏈🏊🏠🏡🏢🏣🏥🏦🏧🏨🏩🏪🏫🏬🏭🏮🏯🏰").show(ui); 66 | EmojiLabel::new("🐌🐍🐎🐑🐒🐔🐗🐘🐙🐚🐛🐜🐝🐞🐟🐠🐡🐢").show(ui); 67 | EmojiLabel::new("🐣🐤🐥🐦🐧🐨🐩🐫🐬🐭🐮🐯🐰🐱🐲🐳🐴🐵").show(ui); 68 | EmojiLabel::new("🐶🐷🐸🐹🐺🐻🐼🐽🐾👀👂👃👄👅👆👇👈👉").show(ui); 69 | EmojiLabel::new("👊👋👌👍👎👏👐👑👒👓👔👕👖👗👘👙👚👛👜").show(ui); 70 | EmojiLabel::new("👝👞👟👠👡👢👣👤👦👧👨👩👪👫👮👯👰👱").show(ui); 71 | EmojiLabel::new("👴👶👷👸👹👺👻👼👽👾👿💀💁💂💃💄💅💆💇").show(ui); 72 | EmojiLabel::new("💈💉💊💋💌💍💎💏💐💑💒💓💔💕💖💗💘💙").show(ui); 73 | EmojiLabel::new("💚💛💜💝💞💟💠💡💢💣💤💥💦💧💨💩💪💫").show(ui); 74 | EmojiLabel::new("💬💮💯💰💲💳💵💸💺💻💼💽💾💿📀📃📅📆📈").show(ui); 75 | EmojiLabel::new("📉📌📍📎📓📔📕📖📞📟📠📡📣📦📧📫📰📱📷📹").show(ui); 76 | EmojiLabel::new("📺📻📼🔊🔋🔌🔎🔐🔑🔒🔓🔔🔜🔥🔦🔧🔨🔩🔪").show(ui); 77 | EmojiLabel::new("🔫🔮🗻🗼🗽🗾🗿😴🚁🚂🚆🚈🚊🚍🚎🚐🚔🚖🚘").show(ui); 78 | EmojiLabel::new("🚛🚜🚝🚞🚟🚠🚡🚣🚦🚮🚵🚿🛁🌍🌎🌜🌝🌞🌲").show(ui); 79 | EmojiLabel::new("🌳🍋🍐🍼🏇🏉🏤🐀🐁🐂🐃🐄🐅🐆🐇🐈🐉🐊🐋").show(ui); 80 | EmojiLabel::new("🐏🐐🐓🐕🐖🐪👬👭📬📭📯🔬🔭").show(ui); 81 | } 82 | -------------------------------------------------------------------------------- /media/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeozeozeo/egui-twemoji/d651fe3ca0bae10b12e99a2596d2ad2997ff8b06/media/demo.png -------------------------------------------------------------------------------- /src/exposed.rs: -------------------------------------------------------------------------------- 1 | use egui::RichText; 2 | 3 | // ugh... 4 | 5 | #[derive(Clone, Default, PartialEq)] 6 | pub struct ExposedRichText { 7 | pub text: String, 8 | pub size: Option, 9 | pub extra_letter_spacing: f32, 10 | pub line_height: Option, 11 | pub family: Option, 12 | pub text_style: Option, 13 | pub background_color: egui::Color32, 14 | pub text_color: Option, 15 | pub code: bool, 16 | pub strong: bool, 17 | pub weak: bool, 18 | pub strikethrough: bool, 19 | pub underline: bool, 20 | pub italics: bool, 21 | pub raised: bool, 22 | } 23 | 24 | impl From for ExposedRichText { 25 | fn from(value: RichText) -> Self { 26 | unsafe { std::mem::transmute(value) } 27 | } 28 | } 29 | 30 | impl From for RichText { 31 | fn from(value: ExposedRichText) -> Self { 32 | unsafe { std::mem::transmute(value) } 33 | } 34 | } 35 | 36 | impl ExposedRichText { 37 | pub fn new_keep_properties(text: impl Into, old: &RichText) -> Self { 38 | Self { 39 | text: text.into(), 40 | ..old.clone().into() 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # egui-twemoji 2 | //! 3 | //! An [egui](https://egui.rs/) widget that renders colored [Twemojis](https://github.com/twitter/twemoji). 4 | //! Based on [twemoji-assets](https://github.com/cptpiepmatz/twemoji-assets). 5 | //! 6 | //! ![demo](https://github.com/zeozeozeo/egui-twemoji/blob/master/media/demo.png?raw=true) 7 | //! 8 | //! # How to use 9 | //! 10 | //! Make sure you've installed `egui_extras` image loaders (required for rendering SVG and PNG emotes): 11 | //! 12 | //! ```ignore 13 | //! // don't do this every frame - only when the app is created! 14 | //! egui_extras::install_image_loaders(&cc.egui_ctx); 15 | //! ``` 16 | //! 17 | //! And then: 18 | //! 19 | //! ```rust 20 | //! use egui_twemoji::EmojiLabel; 21 | //! 22 | //! fn show_label(ui: &mut egui::Ui) { 23 | //! EmojiLabel::new("⭐ egui-twemoji 🐦✨").show(ui); 24 | //! } 25 | //! ``` 26 | //! 27 | //! For a more sophisticated example, see the `demo` example (`cargo run --example demo`) 28 | //! 29 | //! `EmojiLabel` supports all functions that a normal 30 | //! [Label](https://docs.rs/egui/latest/egui/widgets/struct.Label.html) does. 31 | //! 32 | //! # Features 33 | //! 34 | //! * `svg`: use SVG emoji assets (`egui_extras/svg` is required) 35 | //! * `png`: use PNG emoji assets (`egui_extras/image` is required) 36 | //! 37 | //! By default, the `svg` feature is activated. 38 | //! 39 | //! # License 40 | //! 41 | //! Unlicense OR MIT OR Apache-2.0 42 | 43 | #![warn(missing_docs)] 44 | 45 | mod exposed; 46 | 47 | use egui::{ImageSource, Layout, RichText, Sense, TextWrapMode}; 48 | use exposed::ExposedRichText; 49 | use unicode_segmentation::UnicodeSegmentation; 50 | 51 | #[cfg(all(feature = "svg", feature = "png"))] 52 | compile_error!("features 'svg' and 'png' are mutually exclusive and cannot be enabled together"); 53 | 54 | /// Represents a segment of text which can be either plain text or an emoji. 55 | /// 56 | /// * `Text` variant wraps the `RichText` struct, which includes text and its styling information. 57 | /// * `Emoji` variant contains a `String` representing the emoji character. 58 | #[derive(PartialEq, Clone)] 59 | enum TextSegment { 60 | Text(RichText), 61 | Emoji(String), 62 | } 63 | 64 | #[inline] 65 | fn is_emoji(text: &str) -> bool { 66 | #[cfg(feature = "svg")] 67 | return twemoji_assets::svg::SvgTwemojiAsset::from_emoji(text).is_some(); 68 | 69 | #[cfg(feature = "png")] 70 | return twemoji_assets::png::PngTwemojiAsset::from_emoji(text).is_some(); 71 | } 72 | 73 | /// Returns a vector of [`TextSegment`]s from a [`RichText`], segmented by emojis. 74 | /// 75 | /// ## Example: 76 | /// 77 | /// "hello 😤 world" -> `[TextSegment::Text("hello "), TextSegment::Emoji("😤"), TextSegment::Text(" world")]` 78 | fn segment_text(input: &RichText) -> Vec { 79 | let mut result = Vec::new(); 80 | let mut text = String::new(); 81 | 82 | for grapheme in UnicodeSegmentation::graphemes(input.text(), true) { 83 | if is_emoji(grapheme) { 84 | if !text.is_empty() { 85 | result.push(TextSegment::Text( 86 | ExposedRichText::new_keep_properties(text.clone(), input).into(), 87 | )); 88 | text.clear(); 89 | } 90 | result.push(TextSegment::Emoji(grapheme.to_string())); 91 | } else { 92 | text.push_str(grapheme); 93 | } 94 | } 95 | 96 | if !text.is_empty() { 97 | result.push(TextSegment::Text( 98 | ExposedRichText::new_keep_properties(text.clone(), input).into(), 99 | )); 100 | } 101 | 102 | result 103 | } 104 | 105 | /// The state of an [EmojiLabel], stored in egui's [`egui::Memory`]. 106 | /// This includes memoized text segments and whether the state was newly created. 107 | #[derive(Default, Clone)] 108 | struct LabelState { 109 | segments: Vec, 110 | is_saved: bool, 111 | } 112 | 113 | impl LabelState { 114 | /// Create a new state from a [`RichText`], segmenting it by emojis. 115 | fn from_text(text: impl Into) -> Self { 116 | let rich_text = text.into(); 117 | Self { 118 | segments: segment_text(&rich_text), 119 | is_saved: false, 120 | } 121 | } 122 | 123 | /// Load the state from egui's [`egui::Memory`]. 124 | fn load(ctx: &egui::Context, id: egui::Id, text: &RichText) -> Self { 125 | ctx.data_mut(|d| { 126 | d.get_temp(id) 127 | .unwrap_or_else(|| Self::from_text(text.clone())) 128 | }) 129 | } 130 | 131 | /// Save the state to egui's [`egui::Memory`]. Only call this if [`Self::is_saved`] is `false`. 132 | fn save(self, ctx: &egui::Context, id: egui::Id) { 133 | ctx.data_mut(|d| d.insert_temp(id, self)); 134 | } 135 | } 136 | 137 | /// An [egui](https://egui.rs/) widget that renders colored [Twemojis](https://github.com/twitter/twemoji). 138 | /// 139 | /// ```rust 140 | /// use egui_twemoji::EmojiLabel; 141 | /// 142 | /// fn show_label(ui: &mut egui::Ui) { 143 | /// EmojiLabel::new("⭐ egui-twemoji 🐦✨").show(ui); 144 | /// } 145 | /// ``` 146 | #[must_use = "You should put this widget in an ui by calling `.show(ui);`"] 147 | pub struct EmojiLabel { 148 | text: RichText, 149 | wrap_mode: Option, 150 | sense: Option, 151 | selectable: Option, 152 | auto_inline: bool, 153 | } 154 | 155 | fn get_source_for_emoji(emoji: &str) -> Option { 156 | #[cfg(feature = "svg")] 157 | { 158 | let svg_data = twemoji_assets::svg::SvgTwemojiAsset::from_emoji(emoji)?; 159 | let source = ImageSource::Bytes { 160 | uri: format!("{emoji}.svg").into(), 161 | bytes: egui::load::Bytes::Static(svg_data.as_bytes()), 162 | }; 163 | Some(source) 164 | } 165 | 166 | #[cfg(feature = "png")] 167 | { 168 | let png_data: &[u8] = twemoji_assets::png::PngTwemojiAsset::from_emoji(emoji)?; 169 | let source = ImageSource::Bytes { 170 | uri: format!("{emoji}.png").into(), 171 | bytes: egui::load::Bytes::Static(png_data), 172 | }; 173 | Some(source) 174 | } 175 | } 176 | 177 | #[inline] 178 | fn empty_response(ctx: egui::Context) -> egui::Response { 179 | egui::Response { 180 | ctx, 181 | layer_id: egui::LayerId::background(), 182 | id: egui::Id::NULL, 183 | rect: egui::Rect::ZERO, 184 | interact_rect: egui::Rect::ZERO, 185 | sense: Sense::click(), 186 | flags: egui::response::Flags::empty(), 187 | interact_pointer_pos: None, 188 | intrinsic_size: None, 189 | } 190 | } 191 | 192 | impl EmojiLabel { 193 | /// Create a new [`EmojiLabel`] from a [`RichText`]. 194 | pub fn new(text: impl Into) -> Self { 195 | Self { 196 | text: text.into(), 197 | wrap_mode: None, 198 | sense: None, 199 | selectable: None, 200 | auto_inline: true, 201 | } 202 | } 203 | 204 | /// Get the text to render as a [str]. 205 | pub fn text(&self) -> &str { 206 | self.text.text() 207 | } 208 | 209 | /// Get the text to render as a [`RichText`]. 210 | pub fn rich_text(&self) -> &RichText { 211 | &self.text 212 | } 213 | 214 | /// Set the wrap mode for the text. 215 | /// 216 | /// By default, [`egui::Ui::wrap_mode`] will be used, which can be overridden with [`egui::Style::wrap_mode`]. 217 | /// 218 | /// Note that any `\n` in the text will always produce a new line. 219 | #[inline] 220 | pub fn wrap_mode(mut self, wrap_mode: TextWrapMode) -> Self { 221 | self.wrap_mode = Some(wrap_mode); 222 | self 223 | } 224 | 225 | /// Set [`Self::wrap_mode`] to [`TextWrapMode::Wrap`]. 226 | #[inline] 227 | pub fn wrap(mut self) -> Self { 228 | self.wrap_mode = Some(TextWrapMode::Wrap); 229 | self 230 | } 231 | 232 | /// Set [`Self::wrap_mode`] to [`TextWrapMode::Truncate`]. 233 | #[inline] 234 | pub fn truncate(mut self) -> Self { 235 | self.wrap_mode = Some(TextWrapMode::Truncate); 236 | self 237 | } 238 | 239 | /// Set [`Self::wrap_mode`] to [`TextWrapMode::Extend`], 240 | /// disabling wrapping and truncating, and instead expanding the parent [`Ui`]. 241 | #[inline] 242 | pub fn extend(mut self) -> Self { 243 | self.wrap_mode = Some(TextWrapMode::Extend); 244 | self 245 | } 246 | 247 | /// Can the user select the text with the mouse? 248 | /// 249 | /// Overrides [`egui::style::Interaction::selectable_labels`]. 250 | #[inline] 251 | pub fn selectable(mut self, selectable: bool) -> Self { 252 | self.selectable = Some(selectable); 253 | self 254 | } 255 | 256 | /// Make the label respond to clicks and/or drags. 257 | /// 258 | /// By default, a label is inert and does not respond to click or drags. 259 | /// By calling this you can turn the label into a button of sorts. 260 | /// This will also give the label the hover-effect of a button, but without the frame. 261 | #[inline] 262 | pub fn sense(mut self, sense: Sense) -> Self { 263 | self.sense = Some(sense); 264 | self 265 | } 266 | 267 | /// Whether the widget should recognize that it is in a horizontal layout and not create a new one. 268 | /// This fixes some wrapping issues with [`egui::Label`]. 269 | /// 270 | /// In vertical layouts, the widget will create a new horizontal layout so text segments stay on the 271 | /// same line. 272 | #[inline] 273 | pub fn auto_inline(mut self, auto_inline: bool) -> Self { 274 | self.auto_inline = auto_inline; 275 | self 276 | } 277 | 278 | fn show_segments(&self, ui: &mut egui::Ui, state: &mut LabelState) -> egui::Response { 279 | let mut resp = empty_response(ui.ctx().clone()); 280 | let font_height = ui.text_style_height(&egui::TextStyle::Body); 281 | 282 | for segment in &state.segments { 283 | ui.spacing_mut().item_spacing.x = 0.0; 284 | match segment { 285 | TextSegment::Text(text) => { 286 | let mut label = egui::Label::new(text.clone()); 287 | if let Some(wrap_mode) = self.wrap_mode { 288 | label = label.wrap_mode(wrap_mode); 289 | } 290 | resp |= ui.add(label); 291 | } 292 | TextSegment::Emoji(emoji) => { 293 | let Some(source) = get_source_for_emoji(emoji) else { 294 | continue; 295 | }; 296 | 297 | let image_rect = ui 298 | .add( 299 | egui::Image::new(source) 300 | .fit_to_exact_size(egui::vec2(font_height, font_height)), 301 | ) 302 | .rect; 303 | 304 | // for emoji selection and copying: 305 | resp |= ui.put( 306 | image_rect, 307 | egui::Label::new(RichText::new(emoji).color(egui::Color32::TRANSPARENT)), 308 | ); 309 | } 310 | } 311 | } 312 | resp 313 | } 314 | 315 | /// Add the label to an [`egui::Ui`]. 316 | pub fn show(self, ui: &mut egui::Ui) -> egui::Response { 317 | let id = egui::Id::new(self.text()); 318 | let mut state = LabelState::load(ui.ctx(), id, &self.text); 319 | 320 | // if the state was newly created, write it back to memory: 321 | if !state.is_saved { 322 | state.is_saved = true; 323 | state.clone().save(ui.ctx(), id); 324 | } 325 | 326 | if ui.layout().is_horizontal() && self.auto_inline { 327 | self.show_segments(ui, &mut state) 328 | } else { 329 | ui.with_layout(Layout::left_to_right(egui::Align::Min), |ui| { 330 | self.show_segments(ui, &mut state) 331 | }) 332 | .inner 333 | } 334 | } 335 | } 336 | 337 | #[cfg(test)] 338 | mod tests { 339 | use super::*; 340 | 341 | #[test] 342 | fn emoji_segmentation() { 343 | let text = "Hello😤world"; 344 | let segments = segment_text(&RichText::new(text)); 345 | assert!( 346 | segments 347 | == vec![ 348 | TextSegment::Text("Hello".into()), 349 | TextSegment::Emoji("😤".to_owned()), 350 | TextSegment::Text("world".into()) 351 | ] 352 | ); 353 | let text = "😅 2,*:привет|3 🤬"; 354 | let segments = segment_text(&RichText::new(text)); 355 | assert!( 356 | segments 357 | == vec![ 358 | TextSegment::Emoji("😅".to_owned()), 359 | TextSegment::Text(" 2,*:привет|3 ".into()), 360 | TextSegment::Emoji("🤬".to_owned()), 361 | ] 362 | ); 363 | let text = "Hello world 🥰!"; 364 | let segments = segment_text(&RichText::new(text)); 365 | assert!( 366 | segments 367 | == vec![ 368 | TextSegment::Text("Hello world ".into()), 369 | TextSegment::Emoji("🥰".to_owned()), 370 | TextSegment::Text("!".into()) 371 | ] 372 | ); 373 | } 374 | } 375 | --------------------------------------------------------------------------------