diff --git a/crates/bootstrap/Cargo.toml b/crates/bootstrap/Cargo.toml index 4a860be1..f57210b4 100644 --- a/crates/bootstrap/Cargo.toml +++ b/crates/bootstrap/Cargo.toml @@ -10,6 +10,7 @@ profile = ["profiling/profile-with-tracy", "tracy-client"] [dependencies] yakui = { path = "../yakui" } yakui-app = { path = "../yakui-app" } +yakui-widgets = { path = "../yakui-widgets" } yakui-wgpu = { path = "../yakui-wgpu" } yakui-winit = { path = "../yakui-winit" } diff --git a/crates/bootstrap/assets/OpenMoji-LICENSE.txt b/crates/bootstrap/assets/OpenMoji-LICENSE.txt new file mode 100644 index 00000000..cc3e2459 --- /dev/null +++ b/crates/bootstrap/assets/OpenMoji-LICENSE.txt @@ -0,0 +1,427 @@ +Attribution-ShareAlike 4.0 International + +======================================================================= + +Creative Commons Corporation ("Creative Commons") is not a law firm and +does not provide legal services or legal advice. Distribution of +Creative Commons public licenses does not create a lawyer-client or +other relationship. Creative Commons makes its licenses and related +information available on an "as-is" basis. Creative Commons gives no +warranties regarding its licenses, any material licensed under their +terms and conditions, or any related information. Creative Commons +disclaims all liability for damages resulting from their use to the +fullest extent possible. + +Using Creative Commons Public Licenses + +Creative Commons public licenses provide a standard set of terms and +conditions that creators and other rights holders may use to share +original works of authorship and other material subject to copyright +and certain other rights specified in the public license below. The +following considerations are for informational purposes only, are not +exhaustive, and do not form part of our licenses. + + Considerations for licensors: Our public licenses are + intended for use by those authorized to give the public + permission to use material in ways otherwise restricted by + copyright and certain other rights. Our licenses are + irrevocable. Licensors should read and understand the terms + and conditions of the license they choose before applying it. + Licensors should also secure all rights necessary before + applying our licenses so that the public can reuse the + material as expected. Licensors should clearly mark any + material not subject to the license. This includes other CC- + licensed material, or material used under an exception or + limitation to copyright. More considerations for licensors: + wiki.creativecommons.org/Considerations_for_licensors + + Considerations for the public: By using one of our public + licenses, a licensor grants the public permission to use the + licensed material under specified terms and conditions. If + the licensor's permission is not necessary for any reason--for + example, because of any applicable exception or limitation to + copyright--then that use is not regulated by the license. Our + licenses grant only permissions under copyright and certain + other rights that a licensor has authority to grant. Use of + the licensed material may still be restricted for other + reasons, including because others have copyright or other + rights in the material. A licensor may make special requests, + such as asking that all changes be marked or described. + Although not required by our licenses, you are encouraged to + respect those requests where reasonable. More considerations + for the public: + wiki.creativecommons.org/Considerations_for_licensees + +======================================================================= + +Creative Commons Attribution-ShareAlike 4.0 International Public +License + +By exercising the Licensed Rights (defined below), You accept and agree +to be bound by the terms and conditions of this Creative Commons +Attribution-ShareAlike 4.0 International Public License ("Public +License"). To the extent this Public License may be interpreted as a +contract, You are granted the Licensed Rights in consideration of Your +acceptance of these terms and conditions, and the Licensor grants You +such rights in consideration of benefits the Licensor receives from +making the Licensed Material available under these terms and +conditions. + + +Section 1 -- Definitions. + + a. Adapted Material means material subject to Copyright and Similar + Rights that is derived from or based upon the Licensed Material + and in which the Licensed Material is translated, altered, + arranged, transformed, or otherwise modified in a manner requiring + permission under the Copyright and Similar Rights held by the + Licensor. For purposes of this Public License, where the Licensed + Material is a musical work, performance, or sound recording, + Adapted Material is always produced where the Licensed Material is + synched in timed relation with a moving image. + + b. Adapter's License means the license You apply to Your Copyright + and Similar Rights in Your contributions to Adapted Material in + accordance with the terms and conditions of this Public License. + + c. BY-SA Compatible License means a license listed at + creativecommons.org/compatiblelicenses, approved by Creative + Commons as essentially the equivalent of this Public License. + + d. Copyright and Similar Rights means copyright and/or similar rights + closely related to copyright including, without limitation, + performance, broadcast, sound recording, and Sui Generis Database + Rights, without regard to how the rights are labeled or + categorized. For purposes of this Public License, the rights + specified in Section 2(b)(1)-(2) are not Copyright and Similar + Rights. + + e. Effective Technological Measures means those measures that, in the + absence of proper authority, may not be circumvented under laws + fulfilling obligations under Article 11 of the WIPO Copyright + Treaty adopted on December 20, 1996, and/or similar international + agreements. + + f. Exceptions and Limitations means fair use, fair dealing, and/or + any other exception or limitation to Copyright and Similar Rights + that applies to Your use of the Licensed Material. + + g. License Elements means the license attributes listed in the name + of a Creative Commons Public License. The License Elements of this + Public License are Attribution and ShareAlike. + + h. Licensed Material means the artistic or literary work, database, + or other material to which the Licensor applied this Public + License. + + i. Licensed Rights means the rights granted to You subject to the + terms and conditions of this Public License, which are limited to + all Copyright and Similar Rights that apply to Your use of the + Licensed Material and that the Licensor has authority to license. + + j. Licensor means the individual(s) or entity(ies) granting rights + under this Public License. + + k. Share means to provide material to the public by any means or + process that requires permission under the Licensed Rights, such + as reproduction, public display, public performance, distribution, + dissemination, communication, or importation, and to make material + available to the public including in ways that members of the + public may access the material from a place and at a time + individually chosen by them. + + l. Sui Generis Database Rights means rights other than copyright + resulting from Directive 96/9/EC of the European Parliament and of + the Council of 11 March 1996 on the legal protection of databases, + as amended and/or succeeded, as well as other essentially + equivalent rights anywhere in the world. + + m. You means the individual or entity exercising the Licensed Rights + under this Public License. Your has a corresponding meaning. + + +Section 2 -- Scope. + + a. License grant. + + 1. Subject to the terms and conditions of this Public License, + the Licensor hereby grants You a worldwide, royalty-free, + non-sublicensable, non-exclusive, irrevocable license to + exercise the Licensed Rights in the Licensed Material to: + + a. reproduce and Share the Licensed Material, in whole or + in part; and + + b. produce, reproduce, and Share Adapted Material. + + 2. Exceptions and Limitations. For the avoidance of doubt, where + Exceptions and Limitations apply to Your use, this Public + License does not apply, and You do not need to comply with + its terms and conditions. + + 3. Term. The term of this Public License is specified in Section + 6(a). + + 4. Media and formats; technical modifications allowed. The + Licensor authorizes You to exercise the Licensed Rights in + all media and formats whether now known or hereafter created, + and to make technical modifications necessary to do so. The + Licensor waives and/or agrees not to assert any right or + authority to forbid You from making technical modifications + necessary to exercise the Licensed Rights, including + technical modifications necessary to circumvent Effective + Technological Measures. For purposes of this Public License, + simply making modifications authorized by this Section 2(a) + (4) never produces Adapted Material. + + 5. Downstream recipients. + + a. Offer from the Licensor -- Licensed Material. Every + recipient of the Licensed Material automatically + receives an offer from the Licensor to exercise the + Licensed Rights under the terms and conditions of this + Public License. + + b. Additional offer from the Licensor -- Adapted Material. + Every recipient of Adapted Material from You + automatically receives an offer from the Licensor to + exercise the Licensed Rights in the Adapted Material + under the conditions of the Adapter's License You apply. + + c. No downstream restrictions. You may not offer or impose + any additional or different terms or conditions on, or + apply any Effective Technological Measures to, the + Licensed Material if doing so restricts exercise of the + Licensed Rights by any recipient of the Licensed + Material. + + 6. No endorsement. Nothing in this Public License constitutes or + may be construed as permission to assert or imply that You + are, or that Your use of the Licensed Material is, connected + with, or sponsored, endorsed, or granted official status by, + the Licensor or others designated to receive attribution as + provided in Section 3(a)(1)(A)(i). + + b. Other rights. + + 1. Moral rights, such as the right of integrity, are not + licensed under this Public License, nor are publicity, + privacy, and/or other similar personality rights; however, to + the extent possible, the Licensor waives and/or agrees not to + assert any such rights held by the Licensor to the limited + extent necessary to allow You to exercise the Licensed + Rights, but not otherwise. + + 2. Patent and trademark rights are not licensed under this + Public License. + + 3. To the extent possible, the Licensor waives any right to + collect royalties from You for the exercise of the Licensed + Rights, whether directly or through a collecting society + under any voluntary or waivable statutory or compulsory + licensing scheme. In all other cases the Licensor expressly + reserves any right to collect such royalties. + + +Section 3 -- License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the +following conditions. + + a. Attribution. + + 1. If You Share the Licensed Material (including in modified + form), You must: + + a. retain the following if it is supplied by the Licensor + with the Licensed Material: + + i. identification of the creator(s) of the Licensed + Material and any others designated to receive + attribution, in any reasonable manner requested by + the Licensor (including by pseudonym if + designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of + warranties; + + v. a URI or hyperlink to the Licensed Material to the + extent reasonably practicable; + + b. indicate if You modified the Licensed Material and + retain an indication of any previous modifications; and + + c. indicate the Licensed Material is licensed under this + Public License, and include the text of, or the URI or + hyperlink to, this Public License. + + 2. You may satisfy the conditions in Section 3(a)(1) in any + reasonable manner based on the medium, means, and context in + which You Share the Licensed Material. For example, it may be + reasonable to satisfy the conditions by providing a URI or + hyperlink to a resource that includes the required + information. + + 3. If requested by the Licensor, You must remove any of the + information required by Section 3(a)(1)(A) to the extent + reasonably practicable. + + b. ShareAlike. + + In addition to the conditions in Section 3(a), if You Share + Adapted Material You produce, the following conditions also apply. + + 1. The Adapter's License You apply must be a Creative Commons + license with the same License Elements, this version or + later, or a BY-SA Compatible License. + + 2. You must include the text of, or the URI or hyperlink to, the + Adapter's License You apply. You may satisfy this condition + in any reasonable manner based on the medium, means, and + context in which You Share Adapted Material. + + 3. You may not offer or impose any additional or different terms + or conditions on, or apply any Effective Technological + Measures to, Adapted Material that restrict exercise of the + rights granted under the Adapter's License You apply. + + +Section 4 -- Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that +apply to Your use of the Licensed Material: + + a. for the avoidance of doubt, Section 2(a)(1) grants You the right + to extract, reuse, reproduce, and Share all or a substantial + portion of the contents of the database; + + b. if You include all or a substantial portion of the database + contents in a database in which You have Sui Generis Database + Rights, then the database in which You have Sui Generis Database + Rights (but not its individual contents) is Adapted Material, + + including for purposes of Section 3(b); and + c. You must comply with the conditions in Section 3(a) if You Share + all or a substantial portion of the contents of the database. + +For the avoidance of doubt, this Section 4 supplements and does not +replace Your obligations under this Public License where the Licensed +Rights include other Copyright and Similar Rights. + + +Section 5 -- Disclaimer of Warranties and Limitation of Liability. + + a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE + EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS + AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF + ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, + IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, + WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR + PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, + ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT + KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT + ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. + + b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE + TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, + NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, + INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, + COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR + USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN + ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR + DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR + IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. + + c. The disclaimer of warranties and limitation of liability provided + above shall be interpreted in a manner that, to the extent + possible, most closely approximates an absolute disclaimer and + waiver of all liability. + + +Section 6 -- Term and Termination. + + a. This Public License applies for the term of the Copyright and + Similar Rights licensed here. However, if You fail to comply with + this Public License, then Your rights under this Public License + terminate automatically. + + b. Where Your right to use the Licensed Material has terminated under + Section 6(a), it reinstates: + + 1. automatically as of the date the violation is cured, provided + it is cured within 30 days of Your discovery of the + violation; or + + 2. upon express reinstatement by the Licensor. + + For the avoidance of doubt, this Section 6(b) does not affect any + right the Licensor may have to seek remedies for Your violations + of this Public License. + + c. For the avoidance of doubt, the Licensor may also offer the + Licensed Material under separate terms or conditions or stop + distributing the Licensed Material at any time; however, doing so + will not terminate this Public License. + + d. Sections 1, 5, 6, 7, and 8 survive termination of this Public + License. + + +Section 7 -- Other Terms and Conditions. + + a. The Licensor shall not be bound by any additional or different + terms or conditions communicated by You unless expressly agreed. + + b. Any arrangements, understandings, or agreements regarding the + Licensed Material not stated herein are separate from and + independent of the terms and conditions of this Public License. + + +Section 8 -- Interpretation. + + a. For the avoidance of doubt, this Public License does not, and + shall not be interpreted to, reduce, limit, restrict, or impose + conditions on any use of the Licensed Material that could lawfully + be made without permission under this Public License. + + b. To the extent possible, if any provision of this Public License is + deemed unenforceable, it shall be automatically reformed to the + minimum extent necessary to make it enforceable. If the provision + cannot be reformed, it shall be severed from this Public License + without affecting the enforceability of the remaining terms and + conditions. + + c. No term or condition of this Public License will be waived and no + failure to comply consented to unless expressly agreed to by the + Licensor. + + d. Nothing in this Public License constitutes or may be interpreted + as a limitation upon, or waiver of, any privileges and immunities + that apply to the Licensor or You, including from the legal + processes of any jurisdiction or authority. + + +======================================================================= + +Creative Commons is not a party to its public +licenses. Notwithstanding, Creative Commons may elect to apply one of +its public licenses to material it publishes and in those instances +will be considered the “Licensor.” The text of the Creative Commons +public licenses is dedicated to the public domain under the CC0 Public +Domain Dedication. Except for the limited purpose of indicating that +material is shared under a Creative Commons public license or as +otherwise permitted by the Creative Commons policies published at +creativecommons.org/policies, Creative Commons does not authorize the +use of the trademark "Creative Commons" or any other trademark or logo +of Creative Commons without its prior written consent including, +without limitation, in connection with any unauthorized modifications +to any of its public licenses or any other arrangements, +understandings, or agreements concerning use of licensed material. For +the avoidance of doubt, this paragraph does not form part of the +public licenses. + +Creative Commons may be contacted at creativecommons.org. diff --git a/crates/bootstrap/assets/OpenMoji-color-glyf_colr_0.ttf b/crates/bootstrap/assets/OpenMoji-color-glyf_colr_0.ttf new file mode 100644 index 00000000..0257e7e2 Binary files /dev/null and b/crates/bootstrap/assets/OpenMoji-color-glyf_colr_0.ttf differ diff --git a/crates/bootstrap/src/lib.rs b/crates/bootstrap/src/lib.rs index e4ff6e8c..1918eac1 100644 --- a/crates/bootstrap/src/lib.rs +++ b/crates/bootstrap/src/lib.rs @@ -1,6 +1,7 @@ mod custom_texture; use std::fmt::Write; +use std::sync::Arc; use std::time::Instant; use winit::{ @@ -10,10 +11,13 @@ use winit::{ }; use winit::window::{Window, WindowAttributes, WindowId}; -use yakui::font::{Font, FontSettings, Fonts}; +use yakui::font::Fonts; use yakui::paint::{Texture, TextureFilter, TextureFormat}; use yakui::{ManagedTextureId, Rect, TextureId, UVec2, Vec2, Yakui}; use yakui_app::Graphics; +use yakui_widgets::cosmic_text::fontdb; + +pub const OPENMOJI: &[u8] = include_bytes!("../assets/OpenMoji-color-glyf_colr_0.ttf"); const MONKEY_PNG: &[u8] = include_bytes!("../assets/monkey.png"); const MONKEY_BLURRED_PNG: &[u8] = include_bytes!("../assets/monkey-blurred.png"); @@ -61,7 +65,11 @@ impl ApplicationHandler for App { let sample_count = get_sample_count(); - let mut app = pollster::block_on(yakui_app::Graphics::new(&window, sample_count)); + let mut app = pollster::block_on(yakui_app::Graphics::new( + &mut self.yak, + &window, + sample_count, + )); // By default, yakui_winit will measure the system's scale factor and pass // it to yakui. @@ -232,13 +240,11 @@ fn run(body: impl ExampleBody) { // Add a custom font for some of the examples. let fonts = yak.dom().get_global_or_init(Fonts::default); - let font = Font::from_bytes( - include_bytes!("../assets/Hack-Regular.ttf").as_slice(), - FontSettings::default(), - ) - .unwrap(); - fonts.add(font, Some("monospace")); + static HACK_REGULAR: &[u8] = include_bytes!("../assets/Hack-Regular.ttf"); + + fonts.load_font_source(fontdb::Source::Binary(Arc::from(&HACK_REGULAR))); + fonts.set_monospace_family("Hack"); // Set up some default state that we'll modify later. let mut app = App { diff --git a/crates/demo/src/main.rs b/crates/demo/src/main.rs index 267b0d01..3caf44ae 100644 --- a/crates/demo/src/main.rs +++ b/crates/demo/src/main.rs @@ -21,7 +21,7 @@ fn app() { async fn run(event_loop: EventLoop<()>, window: Window) { let mut yak = Yakui::new(); - let mut graphics = Graphics::new(&window, 4).await; + let mut graphics = Graphics::new(&mut yak, &window, 4).await; event_loop.set_control_flow(ControlFlow::Poll); event_loop diff --git a/crates/yakui-app/src/lib.rs b/crates/yakui-app/src/lib.rs index 2ff7ee7e..d2f46c15 100644 --- a/crates/yakui-app/src/lib.rs +++ b/crates/yakui-app/src/lib.rs @@ -23,7 +23,7 @@ pub struct Graphics { } impl Graphics { - pub async fn new(window: &Window, sample_count: u32) -> Self { + pub async fn new(state: &mut yakui_core::Yakui, window: &Window, sample_count: u32) -> Self { let mut size = window.inner_size(); // FIXME: On web, we're receiving (0, 0) as the initial size of the @@ -85,7 +85,7 @@ impl Graphics { // yakui_wgpu takes paint output from yakui and renders it for us using // wgpu. - let renderer = yakui_wgpu::YakuiWgpu::new(&device, &queue); + let renderer = yakui_wgpu::YakuiWgpu::new(state, &device, &queue); // yakui_winit processes winit events and applies them to our yakui // state. diff --git a/crates/yakui-core/src/event.rs b/crates/yakui-core/src/event.rs index b039466d..26c52ba4 100644 --- a/crates/yakui-core/src/event.rs +++ b/crates/yakui-core/src/event.rs @@ -100,7 +100,7 @@ pub enum WidgetEvent { }, /// Text was sent to the widget. - TextInput(char), + TextInput(char, Modifiers), /// The widget was focused or unfocused. FocusChanged(bool), diff --git a/crates/yakui-core/src/input/input_state.rs b/crates/yakui-core/src/input/input_state.rs index b163d065..edeb92e0 100644 --- a/crates/yakui-core/src/input/input_state.rs +++ b/crates/yakui-core/src/input/input_state.rs @@ -306,7 +306,7 @@ impl InputState { // Panic safety: if this node is in the layout DOM, it must be // in the DOM. let mut node = dom.get_mut(id).unwrap(); - let event = WidgetEvent::TextInput(c); + let event = WidgetEvent::TextInput(c, self.modifiers.get()); return self.fire_event(dom, layout, id, &mut node, &event); } } diff --git a/crates/yakui-core/src/paint/paint_dom.rs b/crates/yakui-core/src/paint/paint_dom.rs index 5a82b913..b31ded87 100644 --- a/crates/yakui-core/src/paint/paint_dom.rs +++ b/crates/yakui-core/src/paint/paint_dom.rs @@ -14,6 +14,17 @@ use super::layers::PaintLayers; use super::primitives::{PaintMesh, Vertex}; use super::texture::{Texture, TextureChange}; +#[derive(Debug, Clone, Copy, Default)] +/// Contains all information about the limits of the paint device. +pub struct PaintLimits { + /// Maximum texture size of a 1D texture. + pub max_texture_size_1d: u32, + /// Maximum texture size of a 2D texture. + pub max_texture_size_2d: u32, + /// Maximum texture size of a 3D texture. + pub max_texture_size_3d: u32, +} + /// Contains all information about how to paint the current set of widgets. #[derive(Debug)] pub struct PaintDom { @@ -22,6 +33,7 @@ pub struct PaintDom { surface_size: Vec2, unscaled_viewport: Rect, scale_factor: f32, + limits: PaintLimits, layers: PaintLayers, clip_stack: Vec, @@ -36,11 +48,23 @@ impl PaintDom { surface_size: Vec2::ONE, unscaled_viewport: Rect::ONE, scale_factor: 1.0, + limits: PaintLimits::default(), + layers: PaintLayers::new(), clip_stack: Vec::new(), } } + /// Gets the paint limits. + pub fn limits(&self) -> PaintLimits { + self.limits + } + + /// Sets the paint limits, should be called once by rendering backends. + pub fn set_limit(&mut self, limits: PaintLimits) { + self.limits = limits + } + /// Prepares the PaintDom to be updated for the frame. pub fn start(&mut self) { self.texture_edits.clear(); diff --git a/crates/yakui-core/src/paint/texture.rs b/crates/yakui-core/src/paint/texture.rs index f9f29245..25c5c6c0 100644 --- a/crates/yakui-core/src/paint/texture.rs +++ b/crates/yakui-core/src/paint/texture.rs @@ -37,6 +37,10 @@ pub enum TextureFormat { /// color channels are sRGB-encoded. Rgba8Srgb, + /// Red, green, blue, and alpha channels, each represented as a `u8`. The + /// color channels are sRGB-encoded and premultiplied by the alpha. + Rgba8SrgbPremultiplied, + /// A single color channel represented as a `u8`. R8, } diff --git a/crates/yakui-core/src/state.rs b/crates/yakui-core/src/state.rs index 1540ec84..ce9cc4d1 100644 --- a/crates/yakui-core/src/state.rs +++ b/crates/yakui-core/src/state.rs @@ -5,7 +5,7 @@ use crate::geometry::{Rect, Vec2}; use crate::id::ManagedTextureId; use crate::input::InputState; use crate::layout::LayoutDom; -use crate::paint::{PaintDom, Texture}; +use crate::paint::{PaintDom, PaintLimits, Texture}; /// The entrypoint for yakui. #[derive(Debug)] @@ -120,4 +120,9 @@ impl Yakui { pub fn layout_dom(&self) -> &LayoutDom { &self.layout } + + /// Sets the paint limits, should be called once by rendering backends. + pub fn set_paint_limit(&mut self, limits: PaintLimits) { + self.paint.set_limit(limits) + } } diff --git a/crates/yakui-core/tests/regression.rs b/crates/yakui-core/tests/regression.rs index e8bccf39..668a9a1b 100644 --- a/crates/yakui-core/tests/regression.rs +++ b/crates/yakui-core/tests/regression.rs @@ -74,7 +74,7 @@ impl Widget for KeyboardWidget { } fn event(&mut self, _ctx: EventContext<'_>, event: &WidgetEvent) -> EventResponse { - if let WidgetEvent::TextInput(_) = event { + if let WidgetEvent::TextInput(..) = event { self.count.fetch_add(1, Ordering::SeqCst); } diff --git a/crates/yakui-to-image/src/lib.rs b/crates/yakui-to-image/src/lib.rs index 2e8a9cc1..c947597d 100644 --- a/crates/yakui-to-image/src/lib.rs +++ b/crates/yakui-to-image/src/lib.rs @@ -24,6 +24,6 @@ pub fn paint_and_save_to>(state: &mut yakui_core::Yakui, path: P) pub fn paint(state: &mut yakui_core::Yakui) -> RgbaImage { let graphics = pollster::block_on(Graphics::new()); - let mut renderer = yakui_wgpu::YakuiWgpu::new(&graphics.device, &graphics.queue); + let mut renderer = yakui_wgpu::YakuiWgpu::new(state, &graphics.device, &graphics.queue); graphics.paint(state, &mut renderer) } diff --git a/crates/yakui-vulkan/examples/demo.rs b/crates/yakui-vulkan/examples/demo.rs index fab0ac1e..8210221d 100644 --- a/crates/yakui-vulkan/examples/demo.rs +++ b/crates/yakui-vulkan/examples/demo.rs @@ -45,10 +45,11 @@ fn main() { &vulkan_test.device, vulkan_test.present_queue, vulkan_test.device_memory_properties, + vulkan_test.device_properties, ); let mut options = yakui_vulkan::Options::default(); options.render_pass = vulkan_test.render_pass; - let mut yakui_vulkan = YakuiVulkan::new(&vulkan_context, options); + let mut yakui_vulkan = YakuiVulkan::new(&mut yak, &vulkan_context, options); // Prepare for one frame in flight yakui_vulkan.transfers_submitted(); let gui_state = GuiState { @@ -93,6 +94,7 @@ fn main() { &vulkan_test.device, vulkan_test.present_queue, vulkan_test.device_memory_properties, + vulkan_test.device_properties, ); yak.start(); @@ -232,6 +234,7 @@ struct VulkanTest { instance: ash::Instance, surface_loader: ash::khr::surface::Instance, device_memory_properties: vk::PhysicalDeviceMemoryProperties, + device_properties: vk::PhysicalDeviceProperties, present_queue: vk::Queue, @@ -511,6 +514,8 @@ impl VulkanTest { let device_memory_properties = unsafe { instance.get_physical_device_memory_properties(physical_device) }; + let device_properties = unsafe { instance.get_physical_device_properties(physical_device) }; + Self { device, physical_device, @@ -520,6 +525,7 @@ impl VulkanTest { surface_loader, swapchain_info, device_memory_properties, + device_properties, surface, swapchain, present_image_views, diff --git a/crates/yakui-vulkan/shaders/main.frag b/crates/yakui-vulkan/shaders/main.frag index 58976b14..671180c5 100644 --- a/crates/yakui-vulkan/shaders/main.frag +++ b/crates/yakui-vulkan/shaders/main.frag @@ -20,10 +20,17 @@ void main() { } if (workflow == WORKFLOW_TEXT) { - float coverage = texture(textures[texture_id], in_uv).r; - out_color = in_color * coverage; + vec4 coverage = texture(textures[texture_id], in_uv); + + if (in_color.a > 0.0) { + float alpha = max(max(coverage.r, coverage.g), coverage.b) * in_color.a * coverage.a; + + out_color = vec4(in_color.rgb * alpha, alpha); + } else { + out_color = coverage; + } } else { vec4 user_texture = texture(textures[texture_id], in_uv); out_color = in_color * user_texture; } -} \ No newline at end of file +} diff --git a/crates/yakui-vulkan/shaders/main.frag.spv b/crates/yakui-vulkan/shaders/main.frag.spv index af4499f0..d4b5e661 100644 Binary files a/crates/yakui-vulkan/shaders/main.frag.spv and b/crates/yakui-vulkan/shaders/main.frag.spv differ diff --git a/crates/yakui-vulkan/src/lib.rs b/crates/yakui-vulkan/src/lib.rs index 70c48e94..a5bd8236 100644 --- a/crates/yakui-vulkan/src/lib.rs +++ b/crates/yakui-vulkan/src/lib.rs @@ -33,6 +33,7 @@ pub use vulkan_context::VulkanContext; use vulkan_texture::{UploadQueue, NO_TEXTURE_ID}; pub use vulkan_texture::{VulkanTexture, VulkanTextureCreateInfo}; use yakui::geometry::UVec2; +use yakui::paint::PaintLimits; use yakui::{paint::Vertex as YakuiVertex, ManagedTextureId}; /// A struct wrapping everything needed to render yakui on Vulkan. This will be your main entry point. @@ -151,7 +152,17 @@ impl YakuiVulkan { /// ## Safety /// - `vulkan_context` must have valid members /// - the members of `render_surface` must have been created with the same [`ash::Device`] as `vulkan_context`. - pub fn new(vulkan_context: &VulkanContext, options: Options) -> Self { + pub fn new( + state: &mut yakui_core::Yakui, + vulkan_context: &VulkanContext, + options: Options, + ) -> Self { + state.set_paint_limit(PaintLimits { + max_texture_size_1d: vulkan_context.properties.limits.max_image_dimension1_d, + max_texture_size_2d: vulkan_context.properties.limits.max_image_dimension2_d, + max_texture_size_3d: vulkan_context.properties.limits.max_image_dimension3_d, + }); + let device = vulkan_context.device; let descriptors = Descriptors::new(vulkan_context); diff --git a/crates/yakui-vulkan/src/vulkan_context.rs b/crates/yakui-vulkan/src/vulkan_context.rs index 66c47520..d3d8120d 100644 --- a/crates/yakui-vulkan/src/vulkan_context.rs +++ b/crates/yakui-vulkan/src/vulkan_context.rs @@ -18,6 +18,8 @@ pub struct VulkanContext<'a> { pub queue: vk::Queue, /// Memory properties used for [`crate::YakuiVulkan`]'s allocation commands pub memory_properties: vk::PhysicalDeviceMemoryProperties, + /// Device properties used for setting paint limits + pub properties: vk::PhysicalDeviceProperties, } impl<'a> VulkanContext<'a> { @@ -26,11 +28,13 @@ impl<'a> VulkanContext<'a> { device: &'a ash::Device, queue: vk::Queue, memory_properties: vk::PhysicalDeviceMemoryProperties, + properties: vk::PhysicalDeviceProperties, ) -> Self { Self { device, queue, memory_properties, + properties, } } diff --git a/crates/yakui-vulkan/src/vulkan_texture.rs b/crates/yakui-vulkan/src/vulkan_texture.rs index 712bea18..c746db20 100644 --- a/crates/yakui-vulkan/src/vulkan_texture.rs +++ b/crates/yakui-vulkan/src/vulkan_texture.rs @@ -180,6 +180,7 @@ impl VulkanTexture { fn get_format(yakui_format: yakui::paint::TextureFormat) -> vk::Format { match yakui_format { yakui::paint::TextureFormat::Rgba8Srgb => vk::Format::R8G8B8A8_SRGB, + yakui::paint::TextureFormat::Rgba8SrgbPremultiplied => vk::Format::R8G8B8A8_SRGB, yakui::paint::TextureFormat::R8 => vk::Format::R8_UNORM, _ => panic!("Unsupported texture format: {yakui_format:?}"), } diff --git a/crates/yakui-wgpu/shaders/text.wgsl b/crates/yakui-wgpu/shaders/text.wgsl index e4e1db40..51c11934 100644 --- a/crates/yakui-wgpu/shaders/text.wgsl +++ b/crates/yakui-wgpu/shaders/text.wgsl @@ -31,8 +31,13 @@ fn vs_main( @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4 { - let coverage = textureSample(coverage_texture, coverage_sampler, in.texcoord).r; - let alpha = coverage * in.color.a; + let coverage = textureSample(coverage_texture, coverage_sampler, in.texcoord); - return vec4(in.color.rgb * alpha, alpha); + if in.color.a > 0.0 { + let alpha = max(max(coverage.r, coverage.g), coverage.b) * in.color.a * coverage.a; + + return vec4(in.color.rgb * alpha, alpha); + } else { + return coverage; + } } \ No newline at end of file diff --git a/crates/yakui-wgpu/src/lib.rs b/crates/yakui-wgpu/src/lib.rs index a3494888..19e00793 100644 --- a/crates/yakui-wgpu/src/lib.rs +++ b/crates/yakui-wgpu/src/lib.rs @@ -16,7 +16,7 @@ use bytemuck::{Pod, Zeroable}; use glam::UVec2; use thunderdome::{Arena, Index}; use yakui_core::geometry::{Rect, Vec2, Vec4}; -use yakui_core::paint::{PaintDom, Pipeline, Texture, TextureChange, TextureFormat}; +use yakui_core::paint::{PaintDom, PaintLimits, Pipeline, Texture, TextureChange, TextureFormat}; use yakui_core::{ManagedTextureId, TextureId}; use self::bindgroup_cache::TextureBindgroupCache; @@ -67,7 +67,13 @@ impl Vertex { } impl YakuiWgpu { - pub fn new(device: &wgpu::Device, queue: &wgpu::Queue) -> Self { + pub fn new(state: &mut yakui_core::Yakui, device: &wgpu::Device, queue: &wgpu::Queue) -> Self { + state.set_paint_limit(PaintLimits { + max_texture_size_1d: device.limits().max_texture_dimension_1d, + max_texture_size_2d: device.limits().max_texture_dimension_2d, + max_texture_size_3d: device.limits().max_texture_dimension_3d, + }); + let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("yakui Bind Group Layout"), entries: &[ diff --git a/crates/yakui-wgpu/src/texture.rs b/crates/yakui-wgpu/src/texture.rs index 71d275a6..93acc773 100644 --- a/crates/yakui-wgpu/src/texture.rs +++ b/crates/yakui-wgpu/src/texture.rs @@ -109,6 +109,11 @@ fn data_layout(format: TextureFormat, size: UVec2) -> wgpu::ImageDataLayout { bytes_per_row: Some(4 * size.x), rows_per_image: Some(size.y), }, + TextureFormat::Rgba8SrgbPremultiplied => wgpu::ImageDataLayout { + offset: 0, + bytes_per_row: Some(4 * size.x), + rows_per_image: Some(size.y), + }, TextureFormat::R8 => wgpu::ImageDataLayout { offset: 0, bytes_per_row: Some(size.x), @@ -121,6 +126,7 @@ fn data_layout(format: TextureFormat, size: UVec2) -> wgpu::ImageDataLayout { fn wgpu_format(format: TextureFormat) -> wgpu::TextureFormat { match format { TextureFormat::Rgba8Srgb => wgpu::TextureFormat::Rgba8UnormSrgb, + TextureFormat::Rgba8SrgbPremultiplied => wgpu::TextureFormat::Rgba8UnormSrgb, TextureFormat::R8 => wgpu::TextureFormat::R8Unorm, _ => panic!("Unsupported texture format {format:?}"), } @@ -157,6 +163,7 @@ fn premultiply_alpha(texture: &Texture) -> Cow<'_, Texture> { Cow::Owned(texture) } + TextureFormat::Rgba8SrgbPremultiplied => Cow::Borrowed(texture), TextureFormat::R8 => Cow::Borrowed(texture), _ => Cow::Borrowed(texture), } diff --git a/crates/yakui-widgets/Cargo.toml b/crates/yakui-widgets/Cargo.toml index aea8cd4c..5975dabc 100644 --- a/crates/yakui-widgets/Cargo.toml +++ b/crates/yakui-widgets/Cargo.toml @@ -15,9 +15,12 @@ default-fonts = [] [dependencies] yakui-core = { path = "../yakui-core", version = "0.2.0" } -fontdue = "0.8.0" +cosmic-text = { version = "0.12.0", default-features = false, features = [ + "std", + "swash", +] } +sys-locale = "0.3.1" thunderdome = "0.6.0" -smol_str = "0.2.1" [dev-dependencies] yakui = { path = "../yakui" } diff --git a/crates/yakui-widgets/src/font.rs b/crates/yakui-widgets/src/font.rs index b460577f..96e4398e 100644 --- a/crates/yakui-widgets/src/font.rs +++ b/crates/yakui-widgets/src/font.rs @@ -1,13 +1,6 @@ -use std::cell::Ref; use std::cell::RefCell; -use std::collections::HashMap; -use std::fmt; use std::rc::Rc; - -use smol_str::SmolStr; -use thunderdome::{Arena, Index}; - -pub use fontdue::{Font, FontSettings}; +use std::sync::Arc; #[derive(Clone)] pub struct Fonts { @@ -15,104 +8,82 @@ pub struct Fonts { } struct FontsInner { - storage: Arena, - by_name: HashMap, + font_system: cosmic_text::FontSystem, } impl Fonts { #[allow(unused_mut, unused_assignments)] fn new() -> Self { - let mut storage = Arena::new(); - let mut by_name = HashMap::new(); + let mut font_system = cosmic_text::FontSystem::new_with_locale_and_db( + sys_locale::get_locale().unwrap_or(String::from("en-US")), + { + let mut database = cosmic_text::fontdb::Database::default(); + database.set_serif_family(""); + database.set_sans_serif_family(""); + database.set_cursive_family(""); + database.set_fantasy_family(""); + database.set_monospace_family(""); + database + }, + ); #[cfg(feature = "default-fonts")] { static DEFAULT_BYTES: &[u8] = include_bytes!("../assets/Roboto-Regular.ttf"); - let font = Font::from_bytes(DEFAULT_BYTES, FontSettings::default()) - .expect("failed to load built-in font"); - let id = FontId::new(storage.insert(font)); - by_name.insert(FontName::new("default"), id); + font_system + .db_mut() + .load_font_source(cosmic_text::fontdb::Source::Binary(Arc::from( + &DEFAULT_BYTES, + ))); } - let inner = Rc::new(RefCell::new(FontsInner { storage, by_name })); + let inner = Rc::new(RefCell::new(FontsInner { font_system })); Self { inner } } - pub fn add>(&self, font: Font, name: Option) -> FontId { - let mut inner = self.inner.borrow_mut(); - - let id = FontId::new(inner.storage.insert(font)); - if let Some(name) = name { - inner.by_name.insert(name.into(), id); - } - - id - } - - pub fn get(&self, name: &FontName) -> Option> { - let inner = self.inner.borrow(); - - let &id = inner.by_name.get(name)?; - if inner.storage.contains(id.0) { - Some(Ref::map(inner, |inner| inner.storage.get(id.0).unwrap())) - } else { - None - } - } -} + pub fn with_system(&self, f: impl FnOnce(&mut cosmic_text::FontSystem) -> T) -> T { + let mut inner = (*self.inner).borrow_mut(); -impl Default for Fonts { - fn default() -> Self { - Self::new() + f(&mut inner.font_system) } -} -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct FontName(SmolStr); - -impl FontName { - pub fn new>(name: S) -> Self { - Self(name.as_ref().into()) + pub fn load_font_source( + &self, + source: cosmic_text::fontdb::Source, + ) -> Vec { + self.with_system(|font_system| font_system.db_mut().load_font_source(source)) + .to_vec() } - pub fn as_str(&self) -> &str { - self.0.as_str() + /// Sets the family that will be used by `Family::Serif`. + pub fn set_serif_family>(&self, family: S) { + self.with_system(|font_system| font_system.db_mut().set_serif_family(family)); } -} -impl From<&str> for FontName { - fn from(value: &str) -> Self { - Self(value.into()) + /// Sets the family that will be used by `Family::SansSerif`. + pub fn set_sans_serif_family>(&self, family: S) { + self.with_system(|font_system| font_system.db_mut().set_sans_serif_family(family)); } -} -impl From<&String> for FontName { - fn from(value: &String) -> Self { - Self(value.into()) + /// Sets the family that will be used by `Family::Cursive`. + pub fn set_cursive_family>(&self, family: S) { + self.with_system(|font_system| font_system.db_mut().set_cursive_family(family)); } -} -impl fmt::Display for FontName { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.fmt(f) + /// Sets the family that will be used by `Family::Fantasy`. + pub fn set_fantasy_family>(&self, family: S) { + self.with_system(|font_system| font_system.db_mut().set_fantasy_family(family)); } -} - -/// Identifies a font that has been loaded and can be used by yakui. -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -#[repr(transparent)] -pub struct FontId(Index); -impl FontId { - #[inline] - pub(crate) fn new(index: Index) -> Self { - Self(index) + /// Sets the family that will be used by `Family::Monospace`. + pub fn set_monospace_family>(&self, family: S) { + self.with_system(|font_system| font_system.db_mut().set_monospace_family(family)); } } -impl fmt::Debug for FontId { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "FontId({}, {})", self.0.slot(), self.0.generation()) +impl Default for Fonts { + fn default() -> Self { + Self::new() } } diff --git a/crates/yakui-widgets/src/lib.rs b/crates/yakui-widgets/src/lib.rs index 15308993..739e52ca 100644 --- a/crates/yakui-widgets/src/lib.rs +++ b/crates/yakui-widgets/src/lib.rs @@ -17,6 +17,8 @@ pub mod widgets; pub use self::shorthand::*; +pub use cosmic_text; + #[doc(hidden)] pub struct DocTest { state: yakui_core::Yakui, diff --git a/crates/yakui-widgets/src/shapes.rs b/crates/yakui-widgets/src/shapes.rs index 36eb1d47..8f5bdbec 100644 --- a/crates/yakui-widgets/src/shapes.rs +++ b/crates/yakui-widgets/src/shapes.rs @@ -49,8 +49,8 @@ pub fn cross(output: &mut PaintDom, rect: Rect, color: Color) { output.add_mesh(mesh); } -pub fn selection_halo(output: &mut PaintDom, rect: Rect) { - outline(output, rect, 2.0, Color::WHITE); +pub fn selection_halo(output: &mut PaintDom, rect: Rect, color: Color) { + outline(output, rect, 2.0, color); } pub fn outline(output: &mut PaintDom, rect: Rect, w: f32, color: Color) { diff --git a/crates/yakui-widgets/src/shorthand.rs b/crates/yakui-widgets/src/shorthand.rs index bfcbb68f..975dd8d1 100644 --- a/crates/yakui-widgets/src/shorthand.rs +++ b/crates/yakui-widgets/src/shorthand.rs @@ -100,8 +100,8 @@ pub fn label>>(text: S) -> Response { } /// See [TextBox]. -pub fn textbox>(text: S) -> Response { - TextBox::new(text.into()).show() +pub fn textbox(initial_text: &str, updated_text: Option<&str>) -> Response { + TextBox::show_with_text(initial_text, updated_text) } /// See [Flexible]. diff --git a/crates/yakui-widgets/src/style.rs b/crates/yakui-widgets/src/style.rs index ce5ac69c..5fd189f0 100644 --- a/crates/yakui-widgets/src/style.rs +++ b/crates/yakui-widgets/src/style.rs @@ -1,30 +1,44 @@ use yakui_core::geometry::Color; -use crate::font::FontName; - #[derive(Debug, Clone)] #[non_exhaustive] pub struct TextStyle { - pub font: FontName, pub font_size: f32, + pub line_height_override: Option, pub color: Color, - pub align: TextAlignment, + pub attrs: cosmic_text::AttrsOwned, } -impl TextStyle { - pub fn label() -> Self { +impl Default for TextStyle { + fn default() -> Self { Self { - color: Color::WHITE, - font: FontName::new("default"), font_size: 14.0, - align: TextAlignment::Start, + line_height_override: None, + color: Color::WHITE, + attrs: cosmic_text::AttrsOwned { + family_owned: cosmic_text::FamilyOwned::SansSerif, + ..cosmic_text::AttrsOwned::new(cosmic_text::Attrs::new()) + }, } } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum TextAlignment { - Start, - Center, - End, +impl TextStyle { + pub fn label() -> Self { + Self { + ..Default::default() + } + } + + pub fn line_height(&self) -> f32 { + self.line_height_override + .unwrap_or((self.font_size * 1.25).ceil()) + } + + pub fn to_metrics(&self, scale_factor: f32) -> cosmic_text::Metrics { + cosmic_text::Metrics::new( + (self.font_size * scale_factor).ceil(), + (self.line_height() * scale_factor).ceil(), + ) + } } diff --git a/crates/yakui-widgets/src/text_renderer.rs b/crates/yakui-widgets/src/text_renderer.rs index 17bfa278..195528a0 100644 --- a/crates/yakui-widgets/src/text_renderer.rs +++ b/crates/yakui-widgets/src/text_renderer.rs @@ -2,78 +2,176 @@ use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; -use fontdue::layout::GlyphRasterConfig; -use fontdue::Font; -use yakui_core::geometry::{URect, UVec2}; -use yakui_core::paint::{PaintDom, Texture, TextureFormat}; +use yakui_core::geometry::{Rect, URect, UVec2, Vec2}; +use yakui_core::paint::{PaintDom, Texture, TextureFilter, TextureFormat}; use yakui_core::ManagedTextureId; -#[cfg(not(target_arch = "wasm32"))] -const TEXTURE_SIZE: u32 = 4096; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum Kind { + Mask, + Color, +} -// When targeting the web, limit the texture atlas size to 2048x2048 to fit into -// WebGL 2's limitations. In the future, we should introduce a way to query for -// these limits. -#[cfg(target_arch = "wasm32")] -const TEXTURE_SIZE: u32 = 2048; +impl Kind { + fn num_channels(self) -> usize { + match self { + Kind::Mask => 1, + Kind::Color { .. } => 4, + } + } -#[derive(Debug, Clone)] -pub struct TextGlobalState { - pub glyph_cache: Rc>, + fn texture_format(self) -> TextureFormat { + match self { + Kind::Mask => TextureFormat::R8, + Kind::Color => TextureFormat::Rgba8SrgbPremultiplied, + } + } +} + +pub struct GlyphRender { + pub(crate) kind: Kind, + pub rect: URect, + pub offset: Vec2, + pub tex_rect: Rect, + pub texture: ManagedTextureId, } #[derive(Debug)] -pub struct GlyphCache { +pub struct InnerAtlas { + pub(crate) kind: Kind, pub texture: Option, - pub texture_size: UVec2, - glyphs: HashMap, + pub glyph_rects: HashMap, next_pos: UVec2, max_height: u32, } -impl GlyphCache { - pub fn ensure_texture(&mut self, paint: &mut PaintDom) { +impl InnerAtlas { + fn new(kind: Kind) -> Self { + Self { + kind, + texture: None, + glyph_rects: HashMap::new(), + next_pos: UVec2::ZERO, + max_height: 0, + } + } + + fn ensure_texture(&mut self, paint: &mut PaintDom) { + let texture_size = paint.limits().max_texture_size_2d; + if self.texture.is_none() { - let texture = paint.add_texture(Texture::new( - TextureFormat::R8, - UVec2::new(TEXTURE_SIZE, TEXTURE_SIZE), - vec![0; (TEXTURE_SIZE * TEXTURE_SIZE) as usize], - )); - - self.texture = Some(texture); - self.texture_size = UVec2::new(TEXTURE_SIZE, TEXTURE_SIZE); + let mut texture = Texture::new( + self.kind.texture_format(), + UVec2::new(texture_size, texture_size), + vec![0; (texture_size * texture_size) as usize * self.kind.num_channels()], + ); + texture.mag_filter = TextureFilter::Linear; + texture.min_filter = TextureFilter::Linear; + self.texture = Some(paint.add_texture(texture)) } } - pub fn get_or_insert( + fn get_or_insert( &mut self, paint: &mut PaintDom, - font: &Font, - key: GlyphRasterConfig, - ) -> URect { - *self.glyphs.entry(key).or_insert_with(|| { - paint.mark_texture_modified(self.texture.unwrap()); - let texture = paint.texture_mut(self.texture.unwrap()).unwrap(); - - let (metrics, bitmap) = font.rasterize_indexed(key.glyph_index, key.px); - let glyph_size = UVec2::new(metrics.width as u32, metrics.height as u32); - - let glyph_max = self.next_pos + glyph_size; - let pos = if glyph_max.x < self.texture_size.x { - self.next_pos - } else { - UVec2::new(0, self.max_height) - }; - - self.max_height = self.max_height.max(pos.y + glyph_size.y + 1); - self.next_pos = pos + UVec2::new(glyph_size.x + 1, 0); - - let size = texture.size(); - blit(pos, glyph_size, &bitmap, size, texture.data_mut()); - - URect::from_pos_size(pos, glyph_size) + font_system: &mut cosmic_text::FontSystem, + cache: &mut cosmic_text::SwashCache, + glyph: &cosmic_text::LayoutGlyph, + image: Option, + ) -> Result> { + self.ensure_texture(paint); + + let texture_size = paint.texture_mut(self.texture.unwrap()).unwrap().size(); + + let physical_glyph = glyph.physical((0.0, 0.0), 1.0); + if let Some((rect, offset)) = self.glyph_rects.get(&physical_glyph.cache_key).cloned() { + return Ok(GlyphRender { + kind: self.kind, + rect, + offset, + tex_rect: rect.as_rect().div_vec2(texture_size.as_vec2()), + texture: self.texture.unwrap(), + }); + } + + if glyph.color_opt.is_some() { + panic!("glyph should not have color_opt! yakui uses its own color."); + } + + let Some(image) = + image.or_else(|| cache.get_image_uncached(font_system, physical_glyph.cache_key)) + else { + return Err(None); + }; + + match image.content { + cosmic_text::SwashContent::Mask => { + if self.kind != Kind::Mask { + return Err(Some(image)); + } + } + cosmic_text::SwashContent::Color => { + if self.kind != Kind::Color { + return Err(Some(image)); + } + } + cosmic_text::SwashContent::SubpixelMask => { + panic!("yakui does not support SubpixelMask glyph content!") + } + } + + let glyph_size = UVec2::new(image.placement.width, image.placement.height); + + let glyph_max = self.next_pos + glyph_size; + if glyph_max.x >= texture_size.x || glyph_max.y >= texture_size.y { + self.clear(paint); + return self.get_or_insert(paint, font_system, cache, glyph, Some(image)); + } + + let pos = if glyph_max.x < texture_size.x { + self.next_pos + } else { + UVec2::new(0, self.max_height) + }; + + self.max_height = self.max_height.max(pos.y + glyph_size.y + 1); + self.next_pos = pos + UVec2::new(glyph_size.x + 1, 0); + + let num_channels = self.kind.num_channels() as u32; + let scale = UVec2::new(num_channels, 1); + blit( + pos * scale, + glyph_size * scale, + &image.data, + texture_size * scale, + paint.texture_mut(self.texture.unwrap()).unwrap().data_mut(), + ); + paint.mark_texture_modified(self.texture.unwrap()); + + let rect = URect::from_pos_size(pos, glyph_size); + let offset = Vec2::new(image.placement.left as f32, image.placement.top as f32); + + self.glyph_rects + .insert(physical_glyph.cache_key, (rect, offset)); + + Ok(GlyphRender { + kind: self.kind, + rect, + offset, + tex_rect: rect.as_rect().div_vec2(texture_size.as_vec2()), + texture: self.texture.unwrap(), }) } + + fn clear(&mut self, paint: &mut PaintDom) { + self.glyph_rects.clear(); + self.next_pos = UVec2::ZERO; + self.max_height = 0; + + if let Some(id) = self.texture.take() { + paint.remove_texture(id); + } + } } fn blit(pos: UVec2, src_size: UVec2, src: &[u8], dst_size: UVec2, dst: &mut [u8]) { @@ -93,21 +191,86 @@ fn blit(pos: UVec2, src_size: UVec2, src: &[u8], dst_size: UVec2, dst: &mut [u8] } } -impl TextGlobalState { +/// An atlas containing a cache of rasterized glyphs that can be rendered. +#[derive(Debug)] +pub struct TextAtlas { + pub(crate) color_atlas: InnerAtlas, + pub(crate) mask_atlas: InnerAtlas, +} + +impl TextAtlas { + /// Creates a new [`TextAtlas`] with the given [`ColorMode`]. pub fn new() -> Self { - let glyph_cache = GlyphCache { - texture: None, - glyphs: HashMap::new(), - next_pos: UVec2::ONE, - max_height: 0, + let color_atlas = InnerAtlas::new(Kind::Color); + let mask_atlas = InnerAtlas::new(Kind::Mask); + + Self { + color_atlas, + mask_atlas, + } + } +} + +#[derive(Debug)] +pub struct InnerState { + pub atlas: TextAtlas, + pub swash: cosmic_text::SwashCache, +} + +impl InnerState { + pub fn get_or_insert( + &mut self, + paint: &mut PaintDom, + font_system: &mut cosmic_text::FontSystem, + glyph: &cosmic_text::LayoutGlyph, + ) -> Option { + let a = + self.atlas + .mask_atlas + .get_or_insert(paint, font_system, &mut self.swash, glyph, None); + + match a { + Ok(glyph) => Some(glyph), + Err(image) => { + let b = self.atlas.color_atlas.get_or_insert( + paint, + font_system, + &mut self.swash, + glyph, + image, + ); + + b.ok() + } + } + } +} + +#[derive(Debug, Clone)] +pub struct TextGlobalState { + pub inner: Rc>, +} - // Not initializing to zero to avoid divide by zero issues if we do - // intialize the texture incorrectly. - texture_size: UVec2::ONE, +impl TextGlobalState { + pub fn get_or_insert( + &self, + paint: &mut PaintDom, + font_system: &mut cosmic_text::FontSystem, + glyph: &cosmic_text::LayoutGlyph, + ) -> Option { + self.inner + .borrow_mut() + .get_or_insert(paint, font_system, glyph) + } + + pub fn new() -> Self { + let state = InnerState { + swash: cosmic_text::SwashCache::new(), + atlas: TextAtlas::new(), }; Self { - glyph_cache: Rc::new(RefCell::new(glyph_cache)), + inner: Rc::new(RefCell::new(state)), } } } diff --git a/crates/yakui-widgets/src/widgets/button.rs b/crates/yakui-widgets/src/widgets/button.rs index 201aee93..ea7ccbaa 100644 --- a/crates/yakui-widgets/src/widgets/button.rs +++ b/crates/yakui-widgets/src/widgets/button.rs @@ -7,7 +7,7 @@ use yakui_core::widget::{EventContext, Widget}; use yakui_core::{Alignment, Response}; use crate::colors; -use crate::style::{TextAlignment, TextStyle}; +use crate::style::TextStyle; use crate::util::widget; use crate::widgets::Pad; @@ -31,6 +31,7 @@ if yakui::button("Hello").clicked { #[must_use = "yakui widgets do nothing if you don't `show` them"] pub struct Button { pub text: Cow<'static, str>, + pub alignment: Alignment, pub padding: Pad, pub border_radius: f32, pub style: DynamicButtonStyle, @@ -48,11 +49,8 @@ pub struct DynamicButtonStyle { impl Default for DynamicButtonStyle { fn default() -> Self { - let mut text = TextStyle::label(); - text.align = TextAlignment::Center; - Self { - text, + text: TextStyle::label(), fill: Color::GRAY, } } @@ -62,6 +60,7 @@ impl Button { pub fn unstyled(text: impl Into>) -> Self { Self { text: text.into(), + alignment: Alignment::CENTER, padding: Pad::ZERO, border_radius: 0.0, style: DynamicButtonStyle::default(), @@ -86,11 +85,9 @@ impl Button { ..Default::default() }; - let mut text_style = TextStyle::label(); - text_style.align = TextAlignment::Center; - Self { text: text.into(), + alignment: Alignment::CENTER, padding: Pad::balanced(20.0, 10.0), border_radius: 6.0, style, @@ -147,20 +144,12 @@ impl Widget for ButtonWidget { text_style = style.text.clone(); } - let alignment = match text_style.align { - TextAlignment::Start => Alignment::CENTER_LEFT, - TextAlignment::Center => Alignment::CENTER, - TextAlignment::End => Alignment::CENTER_RIGHT, - }; - let mut container = RoundRect::new(self.props.border_radius); container.color = color; container.show_children(|| { crate::pad(self.props.padding, || { - crate::align(alignment, || { - let mut text = RenderText::label(self.props.text.clone()); - text.style = text_style; - text.show(); + crate::align(self.props.alignment, || { + RenderText::with_style(self.props.text.clone(), text_style).show(); }); }); }); diff --git a/crates/yakui-widgets/src/widgets/mod.rs b/crates/yakui-widgets/src/widgets/mod.rs index 2e75a8b7..58ea0bea 100644 --- a/crates/yakui-widgets/src/widgets/mod.rs +++ b/crates/yakui-widgets/src/widgets/mod.rs @@ -21,7 +21,6 @@ mod pad; mod panel; mod reflow; mod render_text; -mod render_textbox; mod round_rect; mod scrollable; mod slider; @@ -56,7 +55,6 @@ pub use self::pad::*; pub use self::panel::*; pub use self::reflow::*; pub use self::render_text::*; -pub use self::render_textbox::*; pub use self::round_rect::*; pub use self::scrollable::*; pub use self::slider::*; diff --git a/crates/yakui-widgets/src/widgets/render_text.rs b/crates/yakui-widgets/src/widgets/render_text.rs index 45055b27..53778c04 100644 --- a/crates/yakui-widgets/src/widgets/render_text.rs +++ b/crates/yakui-widgets/src/widgets/render_text.rs @@ -1,218 +1,233 @@ -use std::borrow::Cow; -use std::cell::RefCell; -use std::fmt; - -use fontdue::layout::{ - CoordinateSystem, HorizontalAlign as FontdueAlign, Layout, LayoutSettings, - TextStyle as FontdueTextStyle, -}; +use std::cell::{Cell, RefCell}; + use yakui_core::geometry::{Color, Constraints, Rect, Vec2}; use yakui_core::paint::{PaintRect, Pipeline}; use yakui_core::widget::{LayoutContext, PaintContext, Widget}; -use yakui_core::Response; +use yakui_core::{Response, TextureId}; -use crate::font::{FontName, Fonts}; -use crate::style::{TextAlignment, TextStyle}; -use crate::text_renderer::TextGlobalState; +use crate::font::Fonts; +use crate::style::TextStyle; +use crate::text_renderer::{GlyphRender, Kind, TextGlobalState}; use crate::util::widget; /** Renders text. You probably want to use [Text][super::Text] instead, which supports features like padding. + +Responds with [RenderTextBoxResponse]. */ -#[derive(Debug)] +#[derive(Debug, Clone, Default)] #[non_exhaustive] #[must_use = "yakui widgets do nothing if you don't `show` them"] pub struct RenderText { - pub text: Cow<'static, str>, + pub text: String, pub style: TextStyle, } -impl RenderText { - pub fn new(size: f32, text: Cow<'static, str>) -> Self { - let mut style = TextStyle::label(); - style.font_size = size; +pub struct RenderTextResponse { + pub size: Option, +} - Self { text, style } +impl RenderText { + pub fn new>(text: S) -> Self { + Self { + text: text.into(), + style: TextStyle::label(), + } } - pub fn label(text: Cow<'static, str>) -> Self { + pub fn with_style>(text: S, style: TextStyle) -> Self { Self { - text, - style: TextStyle::label(), + text: text.into(), + style, } } pub fn show(self) -> Response { - widget::(self) + Self::show_with_scroll(self, None) + } + + pub fn show_with_scroll( + self, + scroll: Option, + ) -> Response { + widget::((self, scroll)) } } +#[derive(Debug)] pub struct RenderTextWidget { props: RenderText, - layout: RefCell, + buffer: RefCell>, + size: Cell>, + last_text: RefCell, + max_size: Cell, Option)>>, + scale_factor: Cell>, + last_scroll: Cell>, + scroll: Option, } -pub type RenderTextResponse = (); - impl Widget for RenderTextWidget { - type Props<'a> = RenderText; + type Props<'a> = (RenderText, Option); type Response = RenderTextResponse; fn new() -> Self { - let layout = Layout::new(CoordinateSystem::PositiveYDown); - Self { - props: RenderText::new(0.0, Cow::Borrowed("")), - layout: RefCell::new(layout), + props: RenderText::new(""), + buffer: RefCell::default(), + size: Cell::default(), + last_text: RefCell::new(String::new()), + max_size: Cell::default(), + scale_factor: Cell::default(), + last_scroll: Cell::default(), + scroll: None, } } - fn update(&mut self, props: Self::Props<'_>) -> Self::Response { + fn update(&mut self, (props, scroll): Self::Props<'_>) -> Self::Response { self.props = props; - } - - fn layout(&self, ctx: LayoutContext<'_>, input: Constraints) -> Vec2 { - let fonts = ctx.dom.get_global_or_init(Fonts::default); + self.scroll = scroll; - let font = match fonts.get(&self.props.style.font) { - Some(font) => font, - None => { - // TODO: Log once that we were unable to find this font. - panic!( - "font `{}` was set, but was not registered", - self.props.style.font - ); - } - }; + Self::Response { + size: self.size.get(), + } + } - let max_width = input + fn layout(&self, ctx: LayoutContext<'_>, constraints: Constraints) -> Vec2 { + let max_width = constraints .max .x .is_finite() - .then_some(input.max.x * ctx.layout.scale_factor()); - let max_height = input + .then_some(constraints.max.x * ctx.layout.scale_factor()); + let max_height = constraints .max .y .is_finite() - .then_some(input.max.y * ctx.layout.scale_factor()); + .then_some(constraints.max.y * ctx.layout.scale_factor()); + let max_size = (max_width, max_height); - let horizontal_align = match self.props.style.align { - TextAlignment::Start => FontdueAlign::Left, - TextAlignment::Center => FontdueAlign::Center, - TextAlignment::End => FontdueAlign::Right, - }; + let fonts = ctx.dom.get_global_or_init(Fonts::default); - let mut text_layout = self.layout.borrow_mut(); - text_layout.reset(&LayoutSettings { - max_width, - max_height, - horizontal_align, - ..LayoutSettings::default() - }); + fonts.with_system(|font_system| { + let mut buffer = self.buffer.take().unwrap_or_else(|| { + cosmic_text::Buffer::new( + font_system, + self.props.style.to_metrics(ctx.layout.scale_factor()), + ) + }); + + if self.scale_factor.get() != Some(ctx.layout.scale_factor()) + || self.max_size.get() != Some(max_size) + { + buffer.set_metrics_and_size( + font_system, + self.props.style.to_metrics(ctx.layout.scale_factor()), + max_width, + max_height, + ); - text_layout.append( - &[&*font], - &FontdueTextStyle::new( - &self.props.text, - (self.props.style.font_size * ctx.layout.scale_factor()).ceil(), - 0, - ), - ); + self.max_size.replace(Some(max_size)); + self.scale_factor.set(Some(ctx.layout.scale_factor())); + } - let offset_x = get_text_layout_offset_x(&text_layout, ctx.layout.scale_factor()); + if self.last_scroll.get() != self.scroll { + if let Some(scroll) = self.scroll { + buffer.set_scroll(scroll); + } - let size = get_text_layout_size(&text_layout, ctx.layout.scale_factor()) - - Vec2::new(offset_x, 0.0); + self.last_scroll.replace(self.scroll); + } - input.constrain_min(size) - } + if self.last_text.borrow().as_str() != self.props.text.as_str() { + buffer.set_text( + font_system, + &self.props.text, + self.props.style.attrs.as_attrs(), + cosmic_text::Shaping::Advanced, + ); - fn paint(&self, mut ctx: PaintContext<'_>) { - let text_layout = self.layout.borrow_mut(); - let offset_x = get_text_layout_offset_x(&text_layout, ctx.layout.scale_factor()); - let layout_node = ctx.layout.get(ctx.dom.current()).unwrap(); + self.last_text.replace(self.props.text.clone()); + } - paint_text( - &mut ctx, - &self.props.style.font, - layout_node.rect.pos() - Vec2::new(offset_x, 0.0), - &text_layout, - self.props.style.color, - ); - } -} + let size = { + let size_x = buffer + .layout_runs() + .map(|layout| layout.line_w) + .max_by(|a, b| a.total_cmp(b)) + .unwrap_or_default(); + + let size_y = buffer.layout_runs().map(|layout| layout.line_height).sum(); + + Vec2::new(size_x, size_y) / ctx.layout.scale_factor() + }; -impl fmt::Debug for RenderTextWidget { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("TextComponent") - .field("props", &self.props) - .field("layout", &"(no debug impl)") - .finish() + let size = constraints.constrain(size); + self.size.set(Some(size)); + + self.buffer.replace(Some(buffer)); + + size + }) } -} -pub(crate) fn get_text_layout_offset_x(text_layout: &Layout, scale_factor: f32) -> f32 { - let offset_x = text_layout - .glyphs() - .iter() - .map(|glyph| glyph.x) - .min_by(|a, b| a.total_cmp(b)) - .unwrap_or_default(); + fn paint(&self, mut ctx: PaintContext<'_>) { + let fonts = ctx.dom.get_global_or_init(Fonts::default); + let layout_node = ctx.layout.get(ctx.dom.current()).unwrap(); - offset_x / scale_factor -} + let buffer_ref = self.buffer.borrow(); + let Some(buffer) = buffer_ref.as_ref() else { + return; + }; -pub(crate) fn get_text_layout_size(text_layout: &Layout, scale_factor: f32) -> Vec2 { - let height = text_layout - .lines() - .iter() - .flat_map(|line_pos_vec| line_pos_vec.iter()) - .map(|line| line.baseline_y - line.min_descent) - .max_by(|a, b| a.total_cmp(b)) - .unwrap_or_default(); - - let width = text_layout - .glyphs() - .iter() - .map(|glyph| glyph.x + glyph.width as f32) - .max_by(|a, b| a.total_cmp(b)) - .unwrap_or_default(); - - Vec2::new(width, height) / scale_factor + fonts.with_system(|font_system| { + let text_global = ctx.dom.get_global_or_init(TextGlobalState::new); + + for layout in buffer.layout_runs() { + for glyph in layout.glyphs { + if let Some(render) = text_global.get_or_insert(ctx.paint, font_system, glyph) { + paint_text( + &mut ctx, + self.props.style.color, + glyph, + render, + layout_node.rect.pos(), + layout.line_y, + ) + } + } + } + }); + } } -pub fn paint_text( +fn paint_text( ctx: &mut PaintContext<'_>, - font: &FontName, - pos: Vec2, - text_layout: &Layout, color: Color, + glyph: &cosmic_text::LayoutGlyph, + render: GlyphRender, + layout_pos: Vec2, + line_y: f32, ) { - let pos = pos.round(); - let fonts = ctx.dom.get_global_or_init(Fonts::default); - let font = match fonts.get(font) { - Some(font) => font, - None => return, - }; - - let text_global = ctx.dom.get_global_or_init(TextGlobalState::new); - let mut glyph_cache = text_global.glyph_cache.borrow_mut(); - glyph_cache.ensure_texture(ctx.paint); - - for glyph in text_layout.glyphs() { - let tex_rect = glyph_cache - .get_or_insert(ctx.paint, &font, glyph.key) - .as_rect() - .div_vec2(glyph_cache.texture_size.as_vec2()); - - let size = Vec2::new(glyph.width as f32, glyph.height as f32) / ctx.layout.scale_factor(); - let pos = pos + Vec2::new(glyph.x, glyph.y) / ctx.layout.scale_factor(); - - let mut rect = PaintRect::new(Rect::from_pos_size(pos, size)); + let inv_scale_factor = 1.0 / ctx.layout.scale_factor(); + + let size = render.rect.size().as_vec2(); + + let physical = glyph.physical((0.0, 0.0), 1.0); + let pos = Vec2::new(physical.x as f32, physical.y as f32); + + let mut rect = PaintRect::new(Rect::from_pos_size( + Vec2::new(pos.x + render.offset.x, pos.y - render.offset.y + line_y) * inv_scale_factor + + layout_pos, + Vec2::new(size.x, size.y) * inv_scale_factor, + )); + + if render.kind == Kind::Mask { rect.color = color; - rect.texture = Some((glyph_cache.texture.unwrap().into(), tex_rect)); - rect.pipeline = Pipeline::Text; - rect.add(ctx.paint); + } else { + rect.color = Color::CLEAR; } + rect.texture = Some((TextureId::Managed(render.texture), render.tex_rect)); + rect.pipeline = Pipeline::Text; + + rect.add(ctx.paint); } diff --git a/crates/yakui-widgets/src/widgets/render_textbox.rs b/crates/yakui-widgets/src/widgets/render_textbox.rs deleted file mode 100644 index 29bc7d34..00000000 --- a/crates/yakui-widgets/src/widgets/render_textbox.rs +++ /dev/null @@ -1,193 +0,0 @@ -use std::cell::RefCell; -use std::fmt; -use std::rc::Rc; - -use fontdue::layout::{CoordinateSystem, Layout, LayoutSettings, TextStyle as FontdueTextStyle}; -use yakui_core::geometry::{Color, Constraints, Rect, Vec2}; -use yakui_core::paint::PaintRect; -use yakui_core::widget::{LayoutContext, PaintContext, Widget}; -use yakui_core::Response; - -use crate::font::Fonts; -use crate::style::TextStyle; -use crate::util::widget; - -use super::render_text::{get_text_layout_size, paint_text}; - -/** -Rendering and layout logic for a textbox, holding no state. - -Responds with [RenderTextBoxResponse]. -*/ -#[derive(Debug, Clone)] -#[non_exhaustive] -#[must_use = "yakui widgets do nothing if you don't `show` them"] -pub struct RenderTextBox { - pub text: String, - pub style: TextStyle, - pub selected: bool, - pub cursor: usize, -} - -impl RenderTextBox { - pub fn new>(text: S) -> Self { - Self { - text: text.into(), - style: TextStyle::label(), - selected: false, - cursor: 0, - } - } - - pub fn show(self) -> Response { - widget::(self) - } -} - -pub struct RenderTextBoxWidget { - props: RenderTextBox, - cursor_pos_size: RefCell<(Vec2, f32)>, - layout: Rc>, -} - -pub struct RenderTextBoxResponse { - /// The fontdue text layout from this text box. This layout will be reset - /// and updated every time the widget updates. - pub layout: Rc>, -} - -impl Widget for RenderTextBoxWidget { - type Props<'a> = RenderTextBox; - type Response = RenderTextBoxResponse; - - fn new() -> Self { - let layout = Layout::new(CoordinateSystem::PositiveYDown); - - Self { - props: RenderTextBox::new(""), - cursor_pos_size: RefCell::new((Vec2::ZERO, 0.0)), - layout: Rc::new(RefCell::new(layout)), - } - } - - fn update(&mut self, props: Self::Props<'_>) -> Self::Response { - self.props = props; - RenderTextBoxResponse { - layout: self.layout.clone(), - } - } - - fn layout(&self, ctx: LayoutContext<'_>, input: Constraints) -> Vec2 { - let fonts = ctx.dom.get_global_or_init(Fonts::default); - let font = match fonts.get(&self.props.style.font) { - Some(font) => font, - None => { - // TODO: Log once that we were unable to find this font. - return input.min; - } - }; - - let text = &self.props.text; - - let (max_width, max_height) = if input.is_bounded() { - ( - Some(input.max.x * ctx.layout.scale_factor()), - Some(input.max.y * ctx.layout.scale_factor()), - ) - } else { - (None, None) - }; - - let font_size = (self.props.style.font_size * ctx.layout.scale_factor()).ceil(); - - let mut text_layout = self.layout.borrow_mut(); - text_layout.reset(&LayoutSettings { - max_width, - max_height, - ..LayoutSettings::default() - }); - text_layout.append(&[&*font], &FontdueTextStyle::new(text, font_size, 0)); - - let lines = text_layout.lines().map(|x| x.as_slice()).unwrap_or(&[]); - let glyphs = text_layout.glyphs(); - - // TODO: This code doesn't account for graphemes with multiple glyphs. - // We should accumulate the total bounding box of all glyphs that - // contribute to a given grapheme. - let cursor_x = if self.props.cursor >= self.props.text.len() { - // If the cursor is after the last character, we can position it at - // the right edge of the last glyph. - text_layout - .glyphs() - .last() - .map(|glyph| glyph.x + glyph.width as f32 + 1.0) - } else { - // ...otherwise, we'll position the cursor just behind the next - // character after the cursor. - text_layout.glyphs().iter().find_map(|glyph| { - if glyph.byte_offset != self.props.cursor { - return None; - } - - Some(glyph.x - 2.0) - }) - }; - - let cursor_line = lines - .iter() - .find(|line| { - let start_byte = glyphs[line.glyph_start].byte_offset; - let end_byte = glyphs[line.glyph_end].byte_offset; - self.props.cursor >= start_byte && self.props.cursor <= end_byte - }) - .or_else(|| lines.last()); - let cursor_y = cursor_line - .map(|line| line.baseline_y - line.max_ascent) - .unwrap_or(0.0); - - let metrics = font.vertical_line_metrics(font_size); - let ascent = metrics.map(|m| m.ascent).unwrap_or(font_size) / ctx.layout.scale_factor(); - let cursor_size = ascent; - - let cursor_pos = Vec2::new(cursor_x.unwrap_or(0.0), cursor_y) / ctx.layout.scale_factor(); - *self.cursor_pos_size.borrow_mut() = (cursor_pos, cursor_size); - - let mut size = get_text_layout_size(&text_layout, ctx.layout.scale_factor()); - size = size.max(Vec2::new(0.0, ascent)); - - input.constrain(size) - } - - fn paint(&self, mut ctx: PaintContext<'_>) { - let text_layout = self.layout.borrow_mut(); - let layout_node = ctx.layout.get(ctx.dom.current()).unwrap(); - - paint_text( - &mut ctx, - &self.props.style.font, - layout_node.rect.pos(), - &text_layout, - self.props.style.color, - ); - - if self.props.selected { - let (pos, size) = *self.cursor_pos_size.borrow(); - - let cursor_pos = layout_node.rect.pos() + pos; - let cursor_size = Vec2::new(1.0, size); - - let mut rect = PaintRect::new(Rect::from_pos_size(cursor_pos, cursor_size)); - rect.color = Color::RED; - rect.add(ctx.paint); - } - } -} - -impl fmt::Debug for RenderTextBoxWidget { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("RenderTextBoxWidget") - .field("props", &self.props) - .field("layout", &"(no debug impl)") - .finish() - } -} diff --git a/crates/yakui-widgets/src/widgets/text.rs b/crates/yakui-widgets/src/widgets/text.rs index c6ca17f0..fa443007 100644 --- a/crates/yakui-widgets/src/widgets/text.rs +++ b/crates/yakui-widgets/src/widgets/text.rs @@ -48,6 +48,14 @@ impl Text { } } + pub fn with_style>>(text: S, style: TextStyle) -> Self { + Self { + text: text.into(), + style, + padding: Pad::ZERO, + } + } + pub fn label(text: Cow<'static, str>) -> Self { Self { text, @@ -81,7 +89,7 @@ impl Widget for TextWidget { fn update(&mut self, props: Self::Props<'_>) -> Self::Response { self.props = props; - let mut render = RenderText::label(self.props.text.clone()); + let mut render = RenderText::new(self.props.text.clone()); render.style = self.props.style.clone(); pad(self.props.padding, || { diff --git a/crates/yakui-widgets/src/widgets/textbox.rs b/crates/yakui-widgets/src/widgets/textbox.rs index 9c941fed..efa47c68 100644 --- a/crates/yakui-widgets/src/widgets/textbox.rs +++ b/crates/yakui-widgets/src/widgets/textbox.rs @@ -1,21 +1,21 @@ -use std::cell::RefCell; +use std::cell::{Cell, RefCell}; use std::mem; -use std::rc::Rc; -use fontdue::layout::{Layout, LinePosition}; +use cosmic_text::Edit; use yakui_core::event::{EventInterest, EventResponse, WidgetEvent}; -use yakui_core::geometry::{Color, Constraints, Vec2}; +use yakui_core::geometry::{Color, Constraints, Rect, Vec2}; use yakui_core::input::{KeyCode, MouseButton}; +use yakui_core::paint::PaintRect; use yakui_core::widget::{EventContext, LayoutContext, PaintContext, Widget}; use yakui_core::Response; -use crate::ignore_debug::IgnoreDebug; -use crate::shapes::RoundedRectangle; +use crate::font::Fonts; +use crate::shapes::{self, RoundedRectangle}; use crate::style::TextStyle; use crate::util::widget; -use crate::{colors, pad, shapes}; +use crate::{colors, pad, use_state}; -use super::{Pad, RenderTextBox}; +use super::{Pad, RenderText}; /** Text that can be edited. @@ -26,44 +26,89 @@ Responds with [TextBoxResponse]. #[non_exhaustive] #[must_use = "yakui widgets do nothing if you don't `show` them"] pub struct TextBox { - pub text: String, + pub update_text: Option, + pub style: TextStyle, pub padding: Pad, pub fill: Option, + pub radius: f32, + + /// Whether or not enter triggers a loss of focus and if shift would be needed to override that + pub inline_edit: bool, + pub multiline: bool, + + pub selection_halo_color: Color, + pub selected_bg_color: Color, + pub cursor_color: Color, + /// Drawn when no text has been set pub placeholder: String, } impl TextBox { - pub fn new>(text: S) -> Self { + pub fn new(update_text: Option) -> Self { Self { - text: text.into(), + update_text, + style: TextStyle::label(), padding: Pad::all(8.0), fill: Some(colors::BACKGROUND_3), + radius: 6.0, + + inline_edit: true, + multiline: false, + + selection_halo_color: Color::WHITE, + selected_bg_color: Color::CORNFLOWER_BLUE.adjust(0.4), + cursor_color: Color::RED, + placeholder: String::new(), } } + pub fn show_with_text( + initial_text: &str, + updated_text: Option<&str>, + ) -> Response { + let first_time = use_state(|| true); + + if first_time.get() { + first_time.set(false); + + TextBox::new(Some(initial_text.into())).show() + } else { + TextBox::new(updated_text.map(Into::into)).show() + } + } + pub fn show(self) -> Response { widget::(self) } } +#[derive(Debug, PartialEq, Eq)] +enum DragState { + None, + DragStart, + Dragging, +} + #[derive(Debug)] pub struct TextBoxWidget { props: TextBox, - updated_text: Option, - selected: bool, - cursor: usize, - text_layout: Option>>>, + active: bool, activated: bool, lost_focus: bool, + drag: DragState, + cosmic_editor: RefCell>>, + max_size: Cell, Option)>>, + text_changed: Cell, + scale_factor: Cell>, } pub struct TextBoxResponse { pub text: Option, - /// Whether the user pressed "Enter" in this box + /// Whether the user pressed "Enter" in this box, only makes sense in inline pub activated: bool, /// Whether the box lost focus pub lost_focus: bool, @@ -75,213 +120,478 @@ impl Widget for TextBoxWidget { fn new() -> Self { Self { - props: TextBox::new(""), - updated_text: None, - selected: false, - cursor: 0, - text_layout: None, + props: TextBox::new(None), + active: false, activated: false, lost_focus: false, + drag: DragState::None, + cosmic_editor: RefCell::new(None), + max_size: Cell::default(), + text_changed: Cell::default(), + scale_factor: Cell::default(), } } fn update(&mut self, props: Self::Props<'_>) -> Self::Response { self.props = props; - let mut text = self.updated_text.as_ref().unwrap_or(&self.props.text); - let use_placeholder = text.is_empty(); - if use_placeholder { - text = &self.props.placeholder; - } + let mut style = self.props.style.clone(); + let mut scroll = None; - // Make sure the cursor is within bounds if the text has changed - self.cursor = self.cursor.min(text.len()); + let mut is_empty = false; - let mut render = RenderTextBox::new(text.clone()); - render.style = self.props.style.clone(); - render.selected = self.selected; - if !use_placeholder { - render.cursor = self.cursor; - } - if use_placeholder { + let text = self.cosmic_editor.borrow().as_ref().map(|editor| { + editor.with_buffer(|buffer| { + scroll = Some(buffer.scroll()); + is_empty = buffer.lines.iter().all(|v| v.text().is_empty()); + + buffer + .lines + .iter() + .map(|v| v.text()) + .collect::>() + .join("\n") + }) + }); + + if is_empty { // Dim towards background - render.style.color = self - .props - .style + style.color = style .color .lerp(&self.props.fill.unwrap_or(Color::CLEAR), 0.75); } + let render_text = text.clone(); pad(self.props.padding, || { - let res = render.show(); - self.text_layout = Some(IgnoreDebug(res.into_inner().layout)); + let render_text = (!is_empty) + .then_some(render_text) + .flatten() + .unwrap_or(self.props.placeholder.clone()); + + RenderText::with_style(render_text, style).show_with_scroll(scroll); }); Self::Response { - text: self.updated_text.take(), + text: if self.text_changed.take() { + text.clone() + } else { + None + }, activated: mem::take(&mut self.activated), lost_focus: mem::take(&mut self.lost_focus), } } fn layout(&self, ctx: LayoutContext<'_>, constraints: Constraints) -> Vec2 { - ctx.layout.enable_clipping(ctx.dom); + let max_width = constraints.max.x.is_finite().then_some( + (constraints.max.x - self.props.padding.offset().x * 2.0) * ctx.layout.scale_factor(), + ); + let max_height = constraints.max.y.is_finite().then_some( + (constraints.max.y - self.props.padding.offset().y * 2.0) * ctx.layout.scale_factor(), + ); + let max_size = (max_width, max_height); + + let fonts = ctx.dom.get_global_or_init(Fonts::default); + + fonts.with_system(|font_system| { + if self.cosmic_editor.borrow().is_none() { + self.cosmic_editor.replace(Some(cosmic_text::Editor::new( + cosmic_text::BufferRef::Owned(cosmic_text::Buffer::new( + font_system, + self.props.style.to_metrics(ctx.layout.scale_factor()), + )), + ))); + } + + if let Some(editor) = self.cosmic_editor.borrow_mut().as_mut() { + if self.scale_factor.get() != Some(ctx.layout.scale_factor()) + || self.max_size.get() != Some(max_size) + { + editor.with_buffer_mut(|buffer| { + buffer.set_metrics( + font_system, + self.props.style.to_metrics(ctx.layout.scale_factor()), + ); + + buffer.set_size(font_system, max_width, max_height); + }); + + self.scale_factor.set(Some(ctx.layout.scale_factor())); + self.max_size.replace(Some(max_size)); + } + + if let Some(new_text) = &self.props.update_text { + self.text_changed.set(true); + + editor.with_buffer_mut(|buffer| { + buffer.set_text( + font_system, + new_text, + self.props.style.attrs.as_attrs(), + cosmic_text::Shaping::Advanced, + ); + }); + + editor.set_cursor(cosmic_text::Cursor::new(0, 0)); + } + } + }); + self.default_layout(ctx, constraints) } - fn paint(&self, mut ctx: PaintContext<'_>) { + fn paint(&self, ctx: PaintContext<'_>) { let layout_node = ctx.layout.get(ctx.dom.current()).unwrap(); - if let Some(fill_color) = self.props.fill { - let mut bg = RoundedRectangle::new(layout_node.rect, 6.0); - bg.color = fill_color; - bg.add(ctx.paint); - } + let fonts = ctx.dom.get_global_or_init(Fonts::default); + fonts.with_system(|font_system| { + if let Some(fill_color) = self.props.fill { + let mut bg = RoundedRectangle::new(layout_node.rect, self.props.radius); + bg.color = fill_color; + bg.add(ctx.paint); + } - let node = ctx.dom.get_current(); - for &child in &node.children { - ctx.paint(child); - } + if let Some(editor) = self.cosmic_editor.borrow_mut().as_mut() { + editor.shape_as_needed(font_system, false); + + let cursor = editor.cursor(); + let selection = editor.selection_bounds(); + editor.with_buffer_mut(|buffer| { + let inv_scale_factor = 1.0 / ctx.layout.scale_factor(); + + if let Some((a, b)) = selection { + for ((x, w), (y, h)) in buffer + .layout_runs() + .flat_map(|layout| { + layout + .highlight(a, b) + .zip(Some((layout.line_top, layout.line_height))) + }) + .filter(|((_, w), ..)| *w > 0.1) + { + let mut bg = PaintRect::new(Rect::from_pos_size( + layout_node.rect.pos() + + self.props.padding.offset() + + Vec2::new(x, y) * inv_scale_factor, + Vec2::new(w, h) * inv_scale_factor, + )); + bg.color = self.props.selected_bg_color; + bg.add(ctx.paint); + } + } - if self.selected { - shapes::selection_halo(ctx.paint, layout_node.rect); + { + if let Some(((x, _), (y, h))) = buffer + .layout_runs() + .flat_map(|layout| { + layout + .highlight(cursor, cursor) + .zip(Some((layout.line_top, layout.line_height))) + }) + .next() + { + let mut bg = PaintRect::new(Rect::from_pos_size( + layout_node.rect.pos() + + self.props.padding.offset() + + Vec2::new(x, y) * inv_scale_factor, + Vec2::new(1.5, h) * inv_scale_factor, + )); + bg.color = self.props.cursor_color; + bg.add(ctx.paint); + } + } + }); + } + }); + + if self.active { + shapes::selection_halo(ctx.paint, layout_node.rect, self.props.selection_halo_color); } + + self.default_paint(ctx); } fn event_interest(&self) -> EventInterest { - EventInterest::MOUSE_INSIDE | EventInterest::FOCUSED_KEYBOARD + EventInterest::MOUSE_INSIDE | EventInterest::FOCUSED_KEYBOARD | EventInterest::MOUSE_MOVE } fn event(&mut self, ctx: EventContext<'_>, event: &WidgetEvent) -> EventResponse { match event { WidgetEvent::FocusChanged(focused) => { - self.selected = *focused; + self.active = *focused; if !*focused { self.lost_focus = true; + if let Some(editor) = self.cosmic_editor.get_mut() { + editor.set_cursor(cosmic_text::Cursor::new(0, 0)); + } } EventResponse::Sink } - WidgetEvent::MouseButtonChanged { - button: MouseButton::One, - inside: true, - down, - position, - .. - } => { - if !down { - return EventResponse::Sink; - } - - ctx.input.set_selection(Some(ctx.dom.current())); - - if let Some(layout) = ctx.layout.get(ctx.dom.current()) { - if let Some(text_layout) = &self.text_layout { - let text_layout = text_layout.borrow(); + WidgetEvent::MouseMoved(Some(position)) => { + if self.drag == DragState::DragStart { + self.drag = DragState::Dragging; + EventResponse::Sink + } else if self.drag == DragState::Dragging { + if let Some(layout) = ctx.layout.get(ctx.dom.current()) { let scale_factor = ctx.layout.scale_factor(); let relative_pos = *position - layout.rect.pos() - self.props.padding.offset(); - let glyph_pos = relative_pos * scale_factor; - - let Some(line) = pick_text_line(&text_layout, glyph_pos.y) else { - return EventResponse::Sink; - }; - - self.cursor = pick_character_on_line( - &text_layout, - line.glyph_start, - line.glyph_end, - glyph_pos.x, - ); - } - } - - EventResponse::Sink - } - - WidgetEvent::KeyChanged { key, down, .. } => match key { - KeyCode::ArrowLeft => { - if *down { - self.move_cursor(-1); + let glyph_pos = (relative_pos * scale_factor).round().as_ivec2(); + + let fonts = ctx.dom.get_global_or_init(Fonts::default); + fonts.with_system(|font_system| { + if let Some(editor) = self.cosmic_editor.get_mut() { + editor.action( + font_system, + cosmic_text::Action::Drag { + x: glyph_pos.x, + y: glyph_pos.y, + }, + ); + } + }); } - EventResponse::Sink - } - - KeyCode::ArrowRight => { - if *down { - self.move_cursor(1); - } - EventResponse::Sink - } - KeyCode::Backspace => { - if *down { - self.delete(-1); - } EventResponse::Sink + } else { + EventResponse::Bubble } + } - KeyCode::Delete => { - if *down { - self.delete(1); - } - EventResponse::Sink + WidgetEvent::MouseButtonChanged { + button: MouseButton::One, + inside, + down, + position, + modifiers, + .. + } => { + if !inside { + return EventResponse::Sink; } - KeyCode::Home => { - if *down { - self.home(); - } - EventResponse::Sink + if let Some(layout) = ctx.layout.get(ctx.dom.current()) { + let scale_factor = ctx.layout.scale_factor(); + let relative_pos = *position - layout.rect.pos() - self.props.padding.offset(); + let glyph_pos = (relative_pos * scale_factor).round().as_ivec2(); + + let fonts = ctx.dom.get_global_or_init(Fonts::default); + fonts.with_system(|font_system| { + if *down { + if self.drag == DragState::None { + self.drag = DragState::DragStart; + } + + if let Some(editor) = self.cosmic_editor.get_mut() { + if modifiers.shift() { + // TODO wait for cosmic text for shift clicking selection + // Madeline Sparkles: emulating this with a drag + editor.action( + font_system, + cosmic_text::Action::Drag { + x: glyph_pos.x, + y: glyph_pos.y, + }, + ); + } else { + editor.action( + font_system, + cosmic_text::Action::Click { + x: glyph_pos.x, + y: glyph_pos.y, + }, + ); + } + } + } else { + self.drag = DragState::None; + } + }); } - KeyCode::End => { - if *down { - self.end(); - } - EventResponse::Sink - } + ctx.input.set_selection(Some(ctx.dom.current())); - KeyCode::Enter | KeyCode::NumpadEnter => { - if *down { - ctx.input.set_selection(None); - self.activated = true; - } - EventResponse::Sink - } + EventResponse::Sink + } - KeyCode::Escape => { - if *down { - ctx.input.set_selection(None); + WidgetEvent::KeyChanged { + key, + down, + modifiers, + .. + } => { + let fonts = ctx.dom.get_global_or_init(Fonts::default); + fonts.with_system(|font_system| { + if let Some(editor) = self.cosmic_editor.get_mut() { + match key { + KeyCode::ArrowLeft => { + if *down { + if modifiers.ctrl() { + editor.action( + font_system, + cosmic_text::Action::Motion( + cosmic_text::Motion::LeftWord, + ), + ); + } else { + editor.action( + font_system, + cosmic_text::Action::Motion(cosmic_text::Motion::Left), + ); + } + } + EventResponse::Sink + } + + KeyCode::ArrowRight => { + if *down { + if modifiers.ctrl() { + editor.action( + font_system, + cosmic_text::Action::Motion( + cosmic_text::Motion::RightWord, + ), + ); + } else { + editor.action( + font_system, + cosmic_text::Action::Motion(cosmic_text::Motion::Right), + ); + } + } + EventResponse::Sink + } + + KeyCode::ArrowUp => { + if *down { + editor.action( + font_system, + cosmic_text::Action::Motion(cosmic_text::Motion::Up), + ); + } + EventResponse::Sink + } + + KeyCode::ArrowDown => { + if *down { + editor.action( + font_system, + cosmic_text::Action::Motion(cosmic_text::Motion::Down), + ); + } + EventResponse::Sink + } + + KeyCode::PageUp => { + if *down { + editor.action( + font_system, + cosmic_text::Action::Motion(cosmic_text::Motion::PageUp), + ); + } + EventResponse::Sink + } + + KeyCode::PageDown => { + if *down { + editor.action( + font_system, + cosmic_text::Action::Motion(cosmic_text::Motion::PageDown), + ); + } + EventResponse::Sink + } + + KeyCode::Backspace => { + if *down { + editor.action(font_system, cosmic_text::Action::Backspace); + self.text_changed.set(true); + } + EventResponse::Sink + } + + KeyCode::Delete => { + if *down { + editor.action(font_system, cosmic_text::Action::Delete); + self.text_changed.set(true); + } + EventResponse::Sink + } + + KeyCode::Home => { + if *down { + editor.action( + font_system, + cosmic_text::Action::Motion(cosmic_text::Motion::Home), + ); + } + EventResponse::Sink + } + + KeyCode::End => { + if *down { + editor.action( + font_system, + cosmic_text::Action::Motion(cosmic_text::Motion::End), + ); + } + EventResponse::Sink + } + + KeyCode::Enter | KeyCode::NumpadEnter => { + if *down { + if self.props.inline_edit { + if self.props.multiline && modifiers.shift() { + editor.action(font_system, cosmic_text::Action::Enter); + self.text_changed.set(true); + } else { + self.activated = true; + ctx.input.set_selection(None); + } + } else { + editor.action(font_system, cosmic_text::Action::Enter); + self.text_changed.set(true); + } + } + EventResponse::Sink + } + + KeyCode::Escape => { + if *down { + editor.action(font_system, cosmic_text::Action::Escape); + if self.props.inline_edit { + ctx.input.set_selection(None); + } + } + EventResponse::Sink + } + _ => EventResponse::Sink, + } + } else { + EventResponse::Bubble } - EventResponse::Sink - } - _ => EventResponse::Sink, - }, - WidgetEvent::TextInput(c) => { + }) + } + WidgetEvent::TextInput(c, modifiers) => { if c.is_control() { return EventResponse::Bubble; } - let text = self - .updated_text - .get_or_insert_with(|| self.props.text.clone()); - - // Before trying to input text, make sure that our cursor fits - // in the string and is not in the middle of a codepoint! - self.cursor = self.cursor.min(text.len()); - while !text.is_char_boundary(self.cursor) { - self.cursor = self.cursor.saturating_sub(1); - } - - if text.is_empty() { - text.push(*c); - } else { - text.insert(self.cursor, *c); - } - - self.cursor += c.len_utf8(); + let fonts = ctx.dom.get_global_or_init(Fonts::default); + fonts.with_system(|font_system| { + if let Some(editor) = self.cosmic_editor.get_mut() { + if modifiers.ctrl() { + if c.eq_ignore_ascii_case(&'a') { + editor.set_selection(cosmic_text::Selection::Line(editor.cursor())); + } + } else { + editor.action(font_system, cosmic_text::Action::Insert(*c)); + self.text_changed.set(true); + } + } + }); EventResponse::Sink } @@ -289,111 +599,3 @@ impl Widget for TextBoxWidget { } } } - -impl TextBoxWidget { - fn move_cursor(&mut self, delta: i32) { - let text = self.updated_text.as_ref().unwrap_or(&self.props.text); - let mut cursor = self.cursor as i32; - let mut remaining = delta.abs(); - - while remaining > 0 { - cursor = cursor.saturating_add(delta.signum()); - cursor = cursor.min(self.props.text.len() as i32); - cursor = cursor.max(0); - self.cursor = cursor as usize; - - if text.is_char_boundary(self.cursor) { - remaining -= 1; - } - } - } - - fn home(&mut self) { - self.cursor = 0; - } - - fn end(&mut self) { - let text = self.updated_text.as_ref().unwrap_or(&self.props.text); - self.cursor = text.len(); - } - - fn delete(&mut self, dir: i32) { - let text = self - .updated_text - .get_or_insert_with(|| self.props.text.clone()); - - let anchor = self.cursor as i32; - let mut end = anchor; - let mut remaining = dir.abs(); - let mut len = 0; - - while remaining > 0 { - end = end.saturating_add(dir.signum()); - end = end.min(self.props.text.len() as i32); - end = end.max(0); - len += 1; - - if text.is_char_boundary(end as usize) { - remaining -= 1; - } - } - - if dir < 0 { - self.cursor = self.cursor.saturating_sub(len); - } - - let min = anchor.min(end) as usize; - let max = anchor.max(end) as usize; - text.replace_range(min..max, ""); - } -} - -fn pick_text_line(layout: &Layout, pos_y: f32) -> Option<&LinePosition> { - let lines = layout.lines()?; - - let mut closest_line = 0; - let mut closest_line_dist = f32::INFINITY; - for (index, line) in lines.iter().enumerate() { - let dist = (pos_y - line.baseline_y).abs(); - if dist < closest_line_dist { - closest_line = index; - closest_line_dist = dist; - } - } - - lines.get(closest_line) -} - -fn pick_character_on_line( - layout: &Layout, - line_glyph_start: usize, - line_glyph_end: usize, - pos_x: f32, -) -> usize { - let mut closest_byte_offset = 0; - let mut closest_dist = f32::INFINITY; - - let possible_positions = layout - .glyphs() - .iter() - .skip(line_glyph_start) - .take(line_glyph_end + 1 - line_glyph_start) - .flat_map(|glyph| { - let before = Vec2::new(glyph.x, glyph.y); - let after = Vec2::new(glyph.x + glyph.width as f32, glyph.y); - [ - (glyph.byte_offset, before), - (glyph.byte_offset + glyph.parent.len_utf8(), after), - ] - }); - - for (byte_offset, glyph_pos) in possible_positions { - let dist = (pos_x - glyph_pos.x).abs(); - if dist < closest_dist { - closest_byte_offset = byte_offset; - closest_dist = dist; - } - } - - closest_byte_offset -} diff --git a/crates/yakui/examples/autofocus.rs b/crates/yakui/examples/autofocus.rs index a6e80339..f36872f8 100644 --- a/crates/yakui/examples/autofocus.rs +++ b/crates/yakui/examples/autofocus.rs @@ -2,11 +2,11 @@ use yakui::widgets::{Pad, TextBox}; use yakui::{center, use_state}; pub fn run() { - let text = use_state(|| "".to_owned()); + let text = use_state(|| String::new()); let autofocus = use_state(|| false); center(|| { - let mut box1 = TextBox::new(text.borrow().as_str()); + let mut box1 = TextBox::with_text("", None); box1.style.font_size = 60.0; box1.padding = Pad::all(50.0); box1.placeholder = "placeholder".into(); diff --git a/crates/yakui/examples/clear_textbox.rs b/crates/yakui/examples/clear_textbox.rs index e48be119..ed162543 100644 --- a/crates/yakui/examples/clear_textbox.rs +++ b/crates/yakui/examples/clear_textbox.rs @@ -2,15 +2,17 @@ use yakui::{button, column, textbox, use_state}; fn run() { column(|| { - let text = use_state(|| "Hello".to_string()); + let text = use_state(|| String::new()); + let clear = use_state(|| false); - let res = textbox(text.borrow().clone()); + let res = textbox("Hello", if clear.get() { Some("") } else { None }); + clear.set(false); if let Some(new_text) = res.into_inner().text { - *text.borrow_mut() = new_text; + text.set(new_text); } if button("Clear").clicked { - text.borrow_mut().clear(); + clear.set(true); } }); } diff --git a/crates/yakui/examples/custom_font.rs b/crates/yakui/examples/custom_font.rs index b8711457..73359f58 100644 --- a/crates/yakui/examples/custom_font.rs +++ b/crates/yakui/examples/custom_font.rs @@ -1,12 +1,13 @@ use yakui::widgets::Text; use yakui::{column, text, Color}; +use yakui_widgets::cosmic_text::FamilyOwned; pub fn run() { column(|| { text(32.0, "Default Font"); let mut text = Text::new(32.0, "Custom Font"); - text.style.font = "monospace".into(); + text.style.attrs.family_owned = FamilyOwned::Monospace; text.style.color = Color::GREEN; text.show(); }); diff --git a/crates/yakui/examples/inputs.rs b/crates/yakui/examples/inputs.rs index 2dfb3673..eaa67168 100644 --- a/crates/yakui/examples/inputs.rs +++ b/crates/yakui/examples/inputs.rs @@ -3,7 +3,7 @@ use yakui::{button, checkbox, label, pad, row, slider, textbox, use_state}; pub fn run() { let checked = use_state(|| false); - let name = use_state(|| String::from("Hello")); + let name = use_state(|| String::new()); let step_size = use_state(|| 0.0); let sliding = use_state(|| 50.0); @@ -18,9 +18,9 @@ pub fn run() { let res = checkbox(checked.get()); checked.set(res.checked); - let res = textbox(name.borrow().clone()); - if let Some(new_name) = res.text.as_ref() { - name.set(new_name.clone()); + let res = textbox("Hello", None); + if let Some(new_text) = res.into_inner().text { + name.set(new_text); } row(|| { diff --git a/crates/yakui/examples/panels.rs b/crates/yakui/examples/panels.rs index d066efce..295ce690 100644 --- a/crates/yakui/examples/panels.rs +++ b/crates/yakui/examples/panels.rs @@ -32,11 +32,11 @@ pub fn run() { row(|| { label("Input"); expanded(|| { - let name = use_state(|| String::from("Hello")); + let name = use_state(|| String::new()); - let res = textbox(name.borrow().clone()); - if let Some(new_name) = res.text.as_ref() { - name.set(new_name.clone()); + let res = textbox("Hello", None); + if let Some(new_text) = res.into_inner().text { + name.set(new_text); } }); }); diff --git a/crates/yakui/examples/text_render.rs b/crates/yakui/examples/text_render.rs new file mode 100644 index 00000000..f379bddf --- /dev/null +++ b/crates/yakui/examples/text_render.rs @@ -0,0 +1,73 @@ +use std::{cell::Cell, sync::Arc}; + +use bootstrap::OPENMOJI; +use yakui::{column, font::Fonts, text, util::widget, widget::Widget, Vec2}; +use yakui_widgets::cosmic_text::fontdb; + +#[derive(Debug)] +struct LoadFontsWidget { + loaded: Cell, +} + +impl Widget for LoadFontsWidget { + type Props<'a> = (); + + type Response = (); + + fn new() -> Self { + Self { + loaded: Cell::default(), + } + } + + fn update(&mut self, _props: Self::Props<'_>) -> Self::Response {} + + fn layout( + &self, + ctx: yakui::widget::LayoutContext<'_>, + _constraints: yakui::Constraints, + ) -> yakui::Vec2 { + if !self.loaded.get() { + let fonts = ctx.dom.get_global_or_init(Fonts::default); + + fonts.load_font_source(fontdb::Source::Binary(Arc::from(&OPENMOJI))); + + self.loaded.set(true); + } + + Vec2::ZERO + } +} + +pub fn run() { + widget::(()); + + column(|| { + text(16.0, "I like to render اللغة العربية in Rust! + +عندما يريد العالم أن \u{202a}يتكلّم \u{202c} ، فهو يتحدّث بلغة يونيكود. تسجّل الآن لحضور المؤتمر الدولي العاشر ليونيكود (Unicode Conference)، الذي سيعقد في 10-12 آذار 1997 بمدينة مَايِنْتْس، ألمانيا. و سيجمع المؤتمر بين خبراء من كافة قطاعات الصناعة على الشبكة العالمية انترنيت ويونيكود، حيث ستتم، على الصعيدين الدولي والمحلي على حد سواء مناقشة سبل استخدام يونكود في النظم القائمة وفيما يخص التطبيقات الحاسوبية، الخطوط، تصميم النصوص والحوسبة متعددة اللغات."); + + text(16.0, "I want more terminals to be able to handle ZWJ sequence emoji characters. For example, the service dog emoji 🐕‍🦺 is actually 3 Unicode characters. Kitty handles this fairly well. All VTE-based terminals, however, show \"🐶🦺\"."); + + text( + 16.0, + " + 《施氏食狮史》 +石室诗士施氏,嗜狮,誓食十狮。 +氏时时适市视狮。 +十时,适十狮适市。 +是时,适施氏适市。 +氏视是十狮,恃矢势,使是十狮逝世。 +氏拾是十狮尸,适石室。 +石室湿,氏使侍拭石室。 +石室拭,氏始试食是十狮。 +食时,始识是十狮尸,实十石狮尸。 +试释是事。 +", + ); + }); +} + +fn main() { + bootstrap::start(run as fn()); +} diff --git a/crates/yakui/examples/textbox.rs b/crates/yakui/examples/textbox.rs index 665f7e19..5e31e656 100644 --- a/crates/yakui/examples/textbox.rs +++ b/crates/yakui/examples/textbox.rs @@ -2,10 +2,10 @@ use yakui::widgets::{Pad, TextBox}; use yakui::{center, use_state}; pub fn run() { - let text = use_state(|| "".to_owned()); + let text = use_state(|| String::new()); center(|| { - let mut my_box = TextBox::new(text.borrow().as_str()); + let mut my_box = TextBox::new(None); my_box.style.font_size = 60.0; my_box.padding = Pad::all(50.0); my_box.placeholder = "placeholder".into();