From 4dd2c3bc6a9291a4db4c04c8cf086aac73dba774 Mon Sep 17 00:00:00 2001 From: StarArawn <toasterthegamer@gmail.com> Date: Fri, 14 Jan 2022 09:34:37 -0500 Subject: [PATCH] Working text measure. --- Cargo.lock | 16 +- .../src/render/unified/font/extract.rs | 9 +- .../src/render/unified/font/font_mapping.rs | 55 +- bevy_kayak_ui/src/render/unified/font/mod.rs | 20 +- examples/bevy.rs | 2 +- examples/clipping.rs | 10 +- examples/counter.rs | 6 +- examples/fold.rs | 25 +- examples/full_ui.rs | 31 +- examples/global_counter.rs | 2 +- examples/hooks.rs | 33 +- examples/if.rs | 2 +- examples/provider.rs | 4 +- examples/text_box.rs | 2 +- examples/todo/delete_button.rs | 1 + examples/todo/todo.rs | 2 +- examples/vec_widget.rs | 2 +- kayak_core/Cargo.toml | 7 +- kayak_core/src/assets.rs | 36 + kayak_core/src/binding.rs | 4 +- kayak_core/src/context.rs | 137 +++- kayak_core/src/flo_binding/CHANGELOG | 6 + kayak_core/src/flo_binding/LICENSE | 203 +++++ kayak_core/src/flo_binding/bind_stream.rs | 217 ++++++ kayak_core/src/flo_binding/binding.rs | 237 ++++++ kayak_core/src/flo_binding/binding_context.rs | 181 +++++ kayak_core/src/flo_binding/bindref.rs | 183 +++++ kayak_core/src/flo_binding/computed.rs | 320 ++++++++ kayak_core/src/flo_binding/follow.rs | 249 ++++++ kayak_core/src/flo_binding/mod.rs | 736 ++++++++++++++++++ kayak_core/src/flo_binding/notify_fn.rs | 24 + kayak_core/src/flo_binding/releasable.rs | 135 ++++ .../src/flo_binding/rope_binding/core.rs | 125 +++ .../src/flo_binding/rope_binding/mod.rs | 11 + .../flo_binding/rope_binding/rope_binding.rs | 255 ++++++ .../rope_binding/rope_binding_mut.rs | 287 +++++++ .../src/flo_binding/rope_binding/stream.rs | 161 ++++ .../flo_binding/rope_binding/stream_state.rs | 24 + .../src/flo_binding/rope_binding/tests.rs | 44 ++ kayak_core/src/flo_binding/traits.rs | 89 +++ kayak_core/src/lib.rs | 2 + kayak_core/src/render_command.rs | 3 +- kayak_core/src/render_primitive.rs | 5 +- kayak_core/src/widget_manager.rs | 15 +- kayak_core/tests/mod.rs | 31 - kayak_font/src/atlas.rs | 6 +- kayak_font/src/font.rs | 59 +- kayak_font/src/glyph.rs | 4 +- kayak_font/src/metrics.rs | 2 +- kayak_font/src/sdf.rs | 4 +- src/widgets/app.rs | 7 +- src/widgets/clip.rs | 4 +- src/widgets/text.rs | 43 +- src/widgets/window.rs | 4 +- 54 files changed, 3873 insertions(+), 209 deletions(-) create mode 100644 kayak_core/src/assets.rs create mode 100644 kayak_core/src/flo_binding/CHANGELOG create mode 100644 kayak_core/src/flo_binding/LICENSE create mode 100644 kayak_core/src/flo_binding/bind_stream.rs create mode 100644 kayak_core/src/flo_binding/binding.rs create mode 100644 kayak_core/src/flo_binding/binding_context.rs create mode 100644 kayak_core/src/flo_binding/bindref.rs create mode 100644 kayak_core/src/flo_binding/computed.rs create mode 100644 kayak_core/src/flo_binding/follow.rs create mode 100644 kayak_core/src/flo_binding/mod.rs create mode 100644 kayak_core/src/flo_binding/notify_fn.rs create mode 100644 kayak_core/src/flo_binding/releasable.rs create mode 100644 kayak_core/src/flo_binding/rope_binding/core.rs create mode 100644 kayak_core/src/flo_binding/rope_binding/mod.rs create mode 100644 kayak_core/src/flo_binding/rope_binding/rope_binding.rs create mode 100644 kayak_core/src/flo_binding/rope_binding/rope_binding_mut.rs create mode 100644 kayak_core/src/flo_binding/rope_binding/stream.rs create mode 100644 kayak_core/src/flo_binding/rope_binding/stream_state.rs create mode 100644 kayak_core/src/flo_binding/rope_binding/tests.rs create mode 100644 kayak_core/src/flo_binding/traits.rs delete mode 100644 kayak_core/tests/mod.rs diff --git a/Cargo.lock b/Cargo.lock index d190fa9..922d117 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1361,17 +1361,6 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "279fb028e20b3c4c320317955b77c5e0c9701f05a1d309905d6fc702cdc5053e" -[[package]] -name = "flo_binding" -version = "2.0.2" -source = "git+https://github.com/StarArawn/flo_binding.git?rev=c78431a56df5ec082b7e1c271871e6c0ac75e81e#c78431a56df5ec082b7e1c271871e6c0ac75e81e" -dependencies = [ - "desync", - "flo_rope", - "futures", - "uuid", -] - [[package]] name = "flo_rope" version = "0.1.0" @@ -1869,11 +1858,14 @@ dependencies = [ "as-any", "bevy", "derivative", - "flo_binding", + "desync", + "flo_rope", + "futures", "kayak_font", "kayak_render_macros", "morphorm", "resources", + "uuid", ] [[package]] diff --git a/bevy_kayak_ui/src/render/unified/font/extract.rs b/bevy_kayak_ui/src/render/unified/font/extract.rs index 9c5e9a6..0cf2527 100644 --- a/bevy_kayak_ui/src/render/unified/font/extract.rs +++ b/bevy_kayak_ui/src/render/unified/font/extract.rs @@ -20,18 +20,19 @@ pub fn extract_texts( _dpi: f32, ) -> Vec<ExtractQuadBundle> { let mut extracted_texts = Vec::new(); - let (background_color, layout, font_size, content, font) = match render_primitive { + let (background_color, layout, font_size, content, font, parent_size) = match render_primitive { RenderPrimitive::Text { color, layout, size, content, font, - } => (color, layout, *size, content, *font), + parent_size, + } => (color, layout, *size, content, font, parent_size), _ => panic!(""), }; - let font_handle = font_mapping.get_handle(font).unwrap(); + let font_handle = font_mapping.get_handle(font.clone()).unwrap(); let font = fonts.get(font_handle.clone()); if font.is_none() { @@ -46,7 +47,7 @@ pub fn extract_texts( CoordinateSystem::PositiveYDown, Alignment::Start, (layout.posx, layout.posy + line_height), - (layout.width, layout.height), + (parent_size.0, parent_size.1), content, line_height, font_size, diff --git a/bevy_kayak_ui/src/render/unified/font/font_mapping.rs b/bevy_kayak_ui/src/render/unified/font/font_mapping.rs index c9cabc0..df0863e 100644 --- a/bevy_kayak_ui/src/render/unified/font/font_mapping.rs +++ b/bevy_kayak_ui/src/render/unified/font/font_mapping.rs @@ -1,43 +1,64 @@ -use bevy::{prelude::Handle, utils::HashMap}; +use bevy::{ + prelude::{Assets, Handle, Res}, + utils::HashMap, +}; use kayak_font::KayakFont; +use crate::BevyContext; + pub struct FontMapping { - count: u16, - font_ids: HashMap<Handle<KayakFont>, u16>, - font_handles: HashMap<u16, Handle<KayakFont>>, + font_ids: HashMap<Handle<KayakFont>, String>, + font_handles: HashMap<String, Handle<KayakFont>>, + new_fonts: Vec<String>, } impl Default for FontMapping { fn default() -> Self { Self { - count: 0, font_ids: HashMap::default(), font_handles: HashMap::default(), + new_fonts: Vec::new(), } } } impl FontMapping { - pub fn add(&mut self, handle: Handle<KayakFont>) -> u16 { + pub fn add(&mut self, key: impl Into<String>, handle: Handle<KayakFont>) { + let key = key.into(); if !self.font_ids.contains_key(&handle) { - let id = self.count; - self.font_ids.insert(handle.clone(), id); - self.font_handles.insert(id, handle); - self.count += 1; - - id - } else { - *self.font_ids.get(&handle).unwrap() + self.font_ids.insert(handle.clone(), key.clone()); + self.new_fonts.push(key.clone()); + self.font_handles.insert(key, handle); } } - pub fn get_handle(&self, id: u16) -> Option<Handle<KayakFont>> { + pub fn get_handle(&self, id: String) -> Option<Handle<KayakFont>> { self.font_handles .get(&id) .and_then(|item| Some(item.clone())) } - pub fn get(&self, font: &Handle<KayakFont>) -> Option<u16> { - self.font_ids.get(font).and_then(|font_id| Some(*font_id)) + pub fn get(&self, font: &Handle<KayakFont>) -> Option<String> { + self.font_ids + .get(font) + .and_then(|font_id| Some(font_id.clone())) + } + + pub(crate) fn add_loaded_to_kayak( + &mut self, + fonts: &Res<Assets<KayakFont>>, + context: &BevyContext, + ) { + if let Ok(mut kayak_context) = context.kayak_context.write() { + let new_fonts = self.new_fonts.drain(..).collect::<Vec<_>>(); + for font_key in new_fonts { + let font_handle = self.font_handles.get(&font_key).unwrap(); + if let Some(font) = fonts.get(font_handle) { + kayak_context.set_asset(font_key, font.clone()); + } else { + self.new_fonts.push(font_key); + } + } + } } } diff --git a/bevy_kayak_ui/src/render/unified/font/mod.rs b/bevy_kayak_ui/src/render/unified/font/mod.rs index 62c8374..8b34dba 100644 --- a/bevy_kayak_ui/src/render/unified/font/mod.rs +++ b/bevy_kayak_ui/src/render/unified/font/mod.rs @@ -1,5 +1,5 @@ use bevy::{ - prelude::{Plugin, Res, ResMut}, + prelude::{Assets, Plugin, Res, ResMut}, render::{ render_asset::RenderAssets, renderer::{RenderDevice, RenderQueue}, @@ -7,11 +7,16 @@ use bevy::{ RenderApp, RenderStage, }, }; -use kayak_font::bevy::{FontTextureCache, KayakFontPlugin}; +use kayak_font::{ + bevy::{FontTextureCache, KayakFontPlugin}, + KayakFont, +}; mod extract; mod font_mapping; +use crate::BevyContext; + use super::pipeline::UnifiedPipeline; pub use extract::extract_texts; pub use font_mapping::*; @@ -22,13 +27,22 @@ pub struct TextRendererPlugin; impl Plugin for TextRendererPlugin { fn build(&self, app: &mut bevy::prelude::App) { app.add_plugin(KayakFontPlugin) - .init_resource::<FontMapping>(); + .init_resource::<FontMapping>() + .add_system(process_loaded_fonts); let render_app = app.sub_app_mut(RenderApp); render_app.add_system_to_stage(RenderStage::Queue, create_and_update_font_cache_texture); } } +fn process_loaded_fonts( + mut font_mapping: ResMut<FontMapping>, + fonts: Res<Assets<KayakFont>>, + context: Res<BevyContext>, +) { + font_mapping.add_loaded_to_kayak(&fonts, &context); +} + fn create_and_update_font_cache_texture( device: Res<RenderDevice>, queue: Res<RenderQueue>, diff --git a/examples/bevy.rs b/examples/bevy.rs index 20c50e8..2028bcd 100644 --- a/examples/bevy.rs +++ b/examples/bevy.rs @@ -29,7 +29,7 @@ fn startup( ) { commands.spawn_bundle(UICameraBundle::new()); - font_mapping.add(asset_server.load("roboto.kayak_font")); + font_mapping.add("Roboto", asset_server.load("roboto.kayak_font")); let context = BevyContext::new(|context| { render! { diff --git a/examples/clipping.rs b/examples/clipping.rs index 96c5922..e92f22a 100644 --- a/examples/clipping.rs +++ b/examples/clipping.rs @@ -20,7 +20,7 @@ fn startup( ) { commands.spawn_bundle(UICameraBundle::new()); - font_mapping.add(asset_server.load("roboto.kayak_font")); + font_mapping.add("Roboto", asset_server.load("roboto.kayak_font")); let handle: Handle<bevy::render::texture::Image> = asset_server.load("kenny/panel_brown.png"); let panel_brown_handle = image_manager.get(&handle); @@ -40,13 +40,7 @@ fn startup( ..Style::default() }; - let clip_styles = Style { - // padding_left: StyleProp::Value(Units::Pixels(25.0)), - // padding_right: StyleProp::Value(Units::Pixels(25.0)), - // padding_top: StyleProp::Value(Units::Pixels(15.0)), - // padding_bottom: StyleProp::Value(Units::Pixels(125.0)), - ..Style::default() - }; + let clip_styles = Style { ..Style::default() }; let lorem_ipsum = r#" Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras sed tellus neque. Proin tempus ligula a mi molestie aliquam. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nullam venenatis consequat ultricies. Sed ac orci purus. Nullam velit nisl, dapibus vel mauris id, dignissim elementum sapien. Vestibulum faucibus sapien ut erat bibendum, id lobortis nisi luctus. Mauris feugiat at lectus at pretium. Pellentesque vitae finibus ante. Nulla non ex neque. Cras varius, lorem facilisis consequat blandit, lorem mauris mollis massa, eget consectetur magna sem vel enim. Nam aliquam risus pulvinar, volutpat leo eget, eleifend urna. Suspendisse in magna sed ligula vehicula volutpat non vitae augue. Phasellus aliquam viverra consequat. Nam rhoncus molestie purus, sed laoreet neque imperdiet eget. Sed egestas metus eget sodales congue. diff --git a/examples/counter.rs b/examples/counter.rs index 773748f..3ec2795 100644 --- a/examples/counter.rs +++ b/examples/counter.rs @@ -24,12 +24,8 @@ fn Counter(context: &mut KayakContext) { }; let button_text_styles = Style { - bottom: StyleProp::Value(Units::Stretch(1.0)), left: StyleProp::Value(Units::Stretch(1.0)), right: StyleProp::Value(Units::Stretch(1.0)), - top: StyleProp::Value(Units::Stretch(1.0)), - width: StyleProp::Value(Units::Pixels(67.0)), - height: StyleProp::Value(Units::Pixels(39.0)), ..Default::default() }; @@ -58,7 +54,7 @@ fn startup( ) { commands.spawn_bundle(UICameraBundle::new()); - font_mapping.add(asset_server.load("roboto.kayak_font")); + font_mapping.add("Roboto", asset_server.load("roboto.kayak_font")); let context = BevyContext::new(|context| { render! { diff --git a/examples/fold.rs b/examples/fold.rs index 2b52dc9..285e8a6 100644 --- a/examples/fold.rs +++ b/examples/fold.rs @@ -7,11 +7,11 @@ use bevy::{ use kayak_ui::{ bevy::{BevyContext, BevyKayakUIPlugin, FontMapping, UICameraBundle}, core::{ + render, rsx, styles::{Style, StyleProp, Units}, - Color, Handler, render, rsx, use_state, widget, EventType, - Index, OnEvent, + use_state, widget, Color, EventType, Handler, Index, OnEvent, }, - widgets::{App, Background, Button, Fold, If, Text, Window} + widgets::{App, Background, Button, Fold, If, Text, Window}, }; #[widget] @@ -42,28 +42,27 @@ fn FolderTree(context: &mut KayakContext) { ..Default::default() }; - // === Folder A === // let fold_a_styles = Some(Style { - background_color: StyleProp::Value(Color::new(0.25882, 0.24314, 0.19608, 1.0)), + background_color: StyleProp::Value(Color::new(0.25882, 0.24314, 0.19608, 1.0)), ..Default::default() }); let fold_a_child_styles = Style { - background_color: StyleProp::Value(Color::new(0.16863, 0.16863, 0.12549, 1.0)), + background_color: StyleProp::Value(Color::new(0.16863, 0.16863, 0.12549, 1.0)), ..fold_child_base_styles.clone() }; let fold_a_child_child_styles = Style { - background_color: StyleProp::Value(Color::new(0.12941, 0.12941, 0.09412, 1.0)), + background_color: StyleProp::Value(Color::new(0.12941, 0.12941, 0.09412, 1.0)), ..fold_a_child_styles.clone() }; // === Folder B === // let fold_b_styles = Style { - background_color: StyleProp::Value(Color::new(0.19608, 0.25882, 0.21569, 1.0)), + background_color: StyleProp::Value(Color::new(0.19608, 0.25882, 0.21569, 1.0)), ..Default::default() }; let fold_b_child_styles = Style { - background_color: StyleProp::Value(Color::new(0.11765, 0.16078, 0.12941, 1.0)), + background_color: StyleProp::Value(Color::new(0.11765, 0.16078, 0.12941, 1.0)), ..fold_child_base_styles.clone() }; @@ -81,15 +80,15 @@ fn FolderTree(context: &mut KayakContext) { // === Folder C === // let fold_c_styles = Some(Style { - background_color: StyleProp::Value(Color::new(0.25882, 0.19608, 0.23529, 1.0)), + background_color: StyleProp::Value(Color::new(0.25882, 0.19608, 0.23529, 1.0)), ..Default::default() }); let fold_c_child_styles = Style { - background_color: StyleProp::Value(Color::new(0.16863, 0.12549, 0.15294, 1.0)), + background_color: StyleProp::Value(Color::new(0.16863, 0.12549, 0.15294, 1.0)), ..fold_child_base_styles.clone() }; let try_style = Style { - color: StyleProp::Value(Color::new(1.0, 0.5, 0.5, 1.0)), + color: StyleProp::Value(Color::new(1.0, 0.5, 0.5, 1.0)), ..text_styles.clone() }; @@ -149,7 +148,7 @@ fn startup( ) { commands.spawn_bundle(UICameraBundle::new()); - font_mapping.add(asset_server.load("roboto.kayak_font")); + font_mapping.add("Roboto", asset_server.load("roboto.kayak_font")); let context = BevyContext::new(|context| { render! { diff --git a/examples/full_ui.rs b/examples/full_ui.rs index 9c8a445..ccbc041 100644 --- a/examples/full_ui.rs +++ b/examples/full_ui.rs @@ -87,9 +87,9 @@ fn startup( ) { commands.spawn_bundle(UICameraBundle::new()); - font_mapping.add(asset_server.load("roboto.kayak_font")); + font_mapping.add("Roboto", asset_server.load("roboto.kayak_font")); let main_font = asset_server.load("antiquity.kayak_font"); - font_mapping.add(main_font.clone()); + font_mapping.add("Antiquity", main_font.clone()); let handle: Handle<bevy::render::texture::Image> = asset_server.load("kenny/panel_brown.png"); let panel_brown_handle = image_manager.get(&handle); @@ -99,15 +99,10 @@ fn startup( layout_type: StyleProp::Value(LayoutType::Column), width: StyleProp::Value(Units::Pixels(512.0)), height: StyleProp::Value(Units::Pixels(512.0)), - min_height: StyleProp::Value(Units::Pixels(0.0)), - padding_left: StyleProp::Value(Units::Stretch(1.0)), - padding_right: StyleProp::Value(Units::Stretch(1.0)), - padding_top: StyleProp::Value(Units::Stretch(1.0)), - padding_bottom: StyleProp::Value(Units::Stretch(1.0)), - ..Style::default() - }; - - let app_styles = Style { + left: StyleProp::Value(Units::Stretch(1.0)), + right: StyleProp::Value(Units::Stretch(1.0)), + top: StyleProp::Value(Units::Stretch(1.0)), + bottom: StyleProp::Value(Units::Stretch(1.0)), padding_left: StyleProp::Value(Units::Stretch(1.0)), padding_right: StyleProp::Value(Units::Stretch(1.0)), padding_top: StyleProp::Value(Units::Stretch(1.0)), @@ -116,21 +111,21 @@ fn startup( }; let header_styles = Style { - width: StyleProp::Value(Units::Pixels(408.0)), - height: StyleProp::Value(Units::Pixels(42.0)), + // width: StyleProp::Value(Units::Pixels(408.0)), + // height: StyleProp::Value(Units::Pixels(42.0)), bottom: StyleProp::Value(Units::Stretch(1.0)), ..Style::default() }; let text_styles = Style { - width: StyleProp::Value(Units::Pixels(56.0)), - height: StyleProp::Value(Units::Pixels(24.0)), + // width: StyleProp::Value(Units::Pixels(56.0)), + // height: StyleProp::Value(Units::Pixels(24.0)), ..Style::default() }; let options_button_text_styles = Style { - width: StyleProp::Value(Units::Pixels(94.0)), - height: StyleProp::Value(Units::Pixels(24.0)), + // width: StyleProp::Value(Units::Pixels(94.0)), + // height: StyleProp::Value(Units::Pixels(24.0)), ..Style::default() }; @@ -142,7 +137,7 @@ fn startup( let main_font_id = font_mapping.get(&main_font); render! { - <App styles={Some(app_styles)}> + <App> <NinePatch styles={Some(nine_patch_styles)} border={Space { diff --git a/examples/global_counter.rs b/examples/global_counter.rs index 6ead855..ac6642b 100644 --- a/examples/global_counter.rs +++ b/examples/global_counter.rs @@ -35,7 +35,7 @@ fn startup( ) { commands.spawn_bundle(UICameraBundle::new()); - font_mapping.add(asset_server.load("roboto.kayak_font")); + font_mapping.add("Roboto", asset_server.load("roboto.kayak_font")); commands.insert_resource(bind(GlobalCount(0))); diff --git a/examples/hooks.rs b/examples/hooks.rs index 0b994bf..7152bde 100644 --- a/examples/hooks.rs +++ b/examples/hooks.rs @@ -16,7 +16,7 @@ use bevy::{ }; use kayak_ui::{ bevy::{BevyContext, BevyKayakUIPlugin, FontMapping, UICameraBundle}, - core::{EventType, Index, OnEvent, render, rsx, use_effect, use_state, widget}, + core::{render, rsx, use_effect, use_state, widget, EventType, Index, OnEvent}, widgets::{App, Button, Text, Window}, }; @@ -81,26 +81,29 @@ fn EffectCounter() { // use the `get` method on the raw state binding instead, to get the actual value. set_effect_count(raw_count.get() * 2); }, - // In order to call this side-effect closure whenever `raw_count` updates, we need to pass it in as a dependency. // Don't worry about the borrow checker here, `raw_count` is automatically cloned internally, so you don't need // to do that yourself. - [raw_count] - // IMPORTANT: - // If a side-effect updates some other state, make sure you do not pass that state in as a dependency unless you have - // some checks in place to prevent an infinite loop! + [raw_count] // IMPORTANT: + // If a side-effect updates some other state, make sure you do not pass that state in as a dependency unless you have + // some checks in place to prevent an infinite loop! ); - // Passing an empty dependency array causes the callback to only run a single time: when the widget is first rendered. - use_effect!(|| { - println!("First!"); - }, []); + use_effect!( + || { + println!("First!"); + }, + [] + ); // Additionally, order matters with these side-effects. They will be ran in the order they are defined. - use_effect!(|| { - println!("Second!"); - }, []); + use_effect!( + || { + println!("Second!"); + }, + [] + ); rsx! { <Window position={(50.0, 225.0)} size={(300.0, 150.0)} title={"Effect Example".to_string()}> @@ -120,7 +123,7 @@ fn startup( ) { commands.spawn_bundle(UICameraBundle::new()); - font_mapping.add(asset_server.load("roboto.kayak_font")); + font_mapping.add("Roboto", asset_server.load("roboto.kayak_font")); let context = BevyContext::new(|context| { render! { @@ -146,4 +149,4 @@ fn main() { .add_plugin(BevyKayakUIPlugin) .add_startup_system(startup) .run(); -} \ No newline at end of file +} diff --git a/examples/if.rs b/examples/if.rs index 2377188..252e7e2 100644 --- a/examples/if.rs +++ b/examples/if.rs @@ -64,7 +64,7 @@ fn startup( ) { commands.spawn_bundle(UICameraBundle::new()); - font_mapping.add(asset_server.load("roboto.kayak_font")); + font_mapping.add("Roboto", asset_server.load("roboto.kayak_font")); let context = BevyContext::new(|context| { render! { diff --git a/examples/provider.rs b/examples/provider.rs index 295f2a6..6990704 100644 --- a/examples/provider.rs +++ b/examples/provider.rs @@ -240,8 +240,8 @@ fn startup( ) { commands.spawn_bundle(UICameraBundle::new()); - font_mapping.add(asset_server.load("roboto.kayak_font")); - + font_mapping.add("Roboto", asset_server.load("roboto.kayak_font")); + let context = BevyContext::new(|context| { render! { <App> diff --git a/examples/text_box.rs b/examples/text_box.rs index e4f7d7e..2507de9 100644 --- a/examples/text_box.rs +++ b/examples/text_box.rs @@ -62,7 +62,7 @@ fn startup( ) { commands.spawn_bundle(UICameraBundle::new()); - font_mapping.add(asset_server.load("roboto.kayak_font")); + font_mapping.add("Roboto", asset_server.load("roboto.kayak_font")); let context = BevyContext::new(|context| { render! { diff --git a/examples/todo/delete_button.rs b/examples/todo/delete_button.rs index d2b332b..ac01130 100644 --- a/examples/todo/delete_button.rs +++ b/examples/todo/delete_button.rs @@ -17,6 +17,7 @@ pub fn DeleteButton(children: Children, styles: Option<Style>) { render_command: StyleProp::Value(RenderCommand::Layout), height: StyleProp::Value(Units::Pixels(32.0)), width: StyleProp::Value(Units::Pixels(30.0)), + left: StyleProp::Value(Units::Stretch(1.0)), ..base_styles }); diff --git a/examples/todo/todo.rs b/examples/todo/todo.rs index 42a341b..19221d9 100644 --- a/examples/todo/todo.rs +++ b/examples/todo/todo.rs @@ -102,7 +102,7 @@ fn startup( ) { commands.spawn_bundle(UICameraBundle::new()); - font_mapping.add(asset_server.load("roboto.kayak_font")); + font_mapping.add("Roboto", asset_server.load("roboto.kayak_font")); let context = BevyContext::new(|context| { render! { diff --git a/examples/vec_widget.rs b/examples/vec_widget.rs index eddaa6a..fd0f7ec 100644 --- a/examples/vec_widget.rs +++ b/examples/vec_widget.rs @@ -14,7 +14,7 @@ fn startup( ) { commands.spawn_bundle(UICameraBundle::new()); - font_mapping.add(asset_server.load("roboto.kayak_font")); + font_mapping.add("Roboto", asset_server.load("roboto.kayak_font")); let context = BevyContext::new(|context| { let data = vec!["Text1", "Text2", "Text3", "Text4"]; diff --git a/kayak_core/Cargo.toml b/kayak_core/Cargo.toml index 610e7c9..d748177 100644 --- a/kayak_core/Cargo.toml +++ b/kayak_core/Cargo.toml @@ -11,10 +11,13 @@ bevy_renderer = ["bevy", "kayak_font/bevy_renderer"] [dependencies] as-any = "0.2" -derivative = "2.2" bevy = { version = "0.6.0", optional = true } -flo_binding = { git = "https://github.com/StarArawn/flo_binding.git", rev = "c78431a56df5ec082b7e1c271871e6c0ac75e81e" } +derivative = "2.2" +desync = { version = "0.7" } +flo_rope = { version = "0.1" } +futures = { version = "0.3" } kayak_font = { path = "../kayak_font" } kayak_render_macros = { path = "../kayak_render_macros" } morphorm = { git = "https://github.com/geom3trik/morphorm", rev = "1243152d4cebea46fd3e5098df26402c73acae91" } resources = "1.1" +uuid = { version = "0.8", features = ["v4"] } diff --git a/kayak_core/src/assets.rs b/kayak_core/src/assets.rs new file mode 100644 index 0000000..3244abb --- /dev/null +++ b/kayak_core/src/assets.rs @@ -0,0 +1,36 @@ +use std::{collections::HashMap, path::PathBuf}; + +use crate::{Binding, MutableBound}; + +pub struct AssetStorage<T> { + assets: HashMap<PathBuf, Binding<Option<T>>>, +} + +impl<T: Clone + PartialEq + Send + Sync + 'static> AssetStorage<T> { + pub fn new() -> Self { + Self { + assets: HashMap::new(), + } + } + + pub fn get_asset(&mut self, key: impl Into<PathBuf>) -> &Binding<Option<T>> { + let key = key.into(); + if self.assets.contains_key(&key) { + self.assets.get(&key).unwrap() + } else { + // Insert new asset if it doesn't exist yet. + self.assets.insert(key.clone(), Binding::new(None)); + self.assets.get(&key).unwrap() + } + } + + pub fn set_asset(&mut self, key: impl Into<PathBuf>, asset: T) { + let key = key.into(); + if self.assets.contains_key(&key) { + let stored_asset = self.assets.get(&key).unwrap(); + stored_asset.set(Some(asset)); + } else { + self.assets.insert(key, Binding::new(Some(asset))); + } + } +} diff --git a/kayak_core/src/binding.rs b/kayak_core/src/binding.rs index c27e59a..77e9535 100644 --- a/kayak_core/src/binding.rs +++ b/kayak_core/src/binding.rs @@ -1,6 +1,8 @@ use std::time::Instant; -pub use flo_binding::{bind, computed, notify, Binding, Bound, Changeable, ComputedBinding, MutableBound, Releasable}; +pub use crate::flo_binding::{ + bind, computed, notify, Binding, Bound, Changeable, ComputedBinding, MutableBound, Releasable, +}; #[derive(Debug, Clone, Copy, PartialEq)] pub struct Debouncer { diff --git a/kayak_core/src/context.rs b/kayak_core/src/context.rs index 23b4980..7e0d186 100644 --- a/kayak_core/src/context.rs +++ b/kayak_core/src/context.rs @@ -1,27 +1,33 @@ +use crate::assets::AssetStorage; use crate::{Binding, Changeable}; use std::collections::HashMap; +use std::path::PathBuf; -use crate::{multi_state::MultiState, widget_manager::WidgetManager, Index, InputEvent, MutableBound, Releasable}; use crate::event_dispatcher::EventDispatcher; +use crate::{ + multi_state::MultiState, widget_manager::WidgetManager, Index, InputEvent, MutableBound, + Releasable, +}; pub struct KayakContext { - widget_states: HashMap<crate::Index, resources::Resources>, + assets: resources::Resources, + current_effect_index: usize, + current_id: Index, + current_state_index: usize, + event_dispatcher: EventDispatcher, + global_bindings: HashMap<crate::Index, Vec<crate::flo_binding::Uuid>>, + global_state: resources::Resources, + last_state_type_id: Option<std::any::TypeId>, + // TODO: Make widget_manager private. + pub widget_manager: WidgetManager, widget_effects: HashMap<crate::Index, resources::Resources>, /// Contains provider state data to be accessed by consumers. /// /// Maps the type of the data to a mapping of the provider node's ID to the state data widget_providers: HashMap<std::any::TypeId, HashMap<crate::Index, resources::Resources>>, - global_bindings: HashMap<crate::Index, Vec<flo_binding::Uuid>>, widget_state_lifetimes: - HashMap<crate::Index, HashMap<flo_binding::Uuid, Box<dyn crate::Releasable>>>, - current_id: Index, - // TODO: Make widget_manager private. - pub widget_manager: WidgetManager, - event_dispatcher: EventDispatcher, - global_state: resources::Resources, - last_state_type_id: Option<std::any::TypeId>, - current_state_index: usize, - current_effect_index: usize, + HashMap<crate::Index, HashMap<crate::flo_binding::Uuid, Box<dyn crate::Releasable>>>, + widget_states: HashMap<crate::Index, resources::Resources>, } impl std::fmt::Debug for KayakContext { @@ -36,25 +42,26 @@ impl KayakContext { /// Creates a new [`KayakContext`]. pub fn new() -> Self { Self { - widget_states: HashMap::new(), - widget_effects: HashMap::new(), - widget_providers: HashMap::new(), - global_bindings: HashMap::new(), - widget_state_lifetimes: HashMap::new(), + assets: resources::Resources::default(), + current_effect_index: 0, current_id: crate::Index::default(), - widget_manager: WidgetManager::new(), + current_state_index: 0, event_dispatcher: EventDispatcher::default(), + global_bindings: HashMap::new(), global_state: resources::Resources::default(), last_state_type_id: None, - current_state_index: 0, - current_effect_index: 0, + widget_effects: HashMap::new(), + widget_manager: WidgetManager::new(), + widget_providers: HashMap::new(), + widget_state_lifetimes: HashMap::new(), + widget_states: HashMap::new(), } } /// Binds some global state to the current widget. pub fn bind<T: Clone + PartialEq + Send + Sync + 'static>( &mut self, - global_state: &crate::Binding<T>, + binding: &crate::Binding<T>, ) { if !self.global_bindings.contains_key(&self.current_id) { self.global_bindings.insert(self.current_id, vec![]); @@ -62,15 +69,15 @@ impl KayakContext { let global_binding_ids = self.global_bindings.get_mut(&self.current_id).unwrap(); - if !global_binding_ids.contains(&global_state.id) { - let lifetime = Self::create_lifetime(&global_state, &self.widget_manager, self.current_id); + if !global_binding_ids.contains(&binding.id) { + let lifetime = Self::create_lifetime(&binding, &self.widget_manager, self.current_id); Self::insert_state_lifetime( &mut self.widget_state_lifetimes, self.current_id, - global_state.id, + binding.id, lifetime, ); - global_binding_ids.push(global_state.id); + global_binding_ids.push(binding.id); } } @@ -99,10 +106,16 @@ impl KayakContext { /// /// This works much like [create_state](Self::create_state), except that the state is also made available to any children. They can /// access this provider's state by calling [create_consumer](Self::create_consumer). - pub fn create_provider<T: resources::Resource + Clone + PartialEq>(&mut self, initial_state: T) -> Binding<T> { + pub fn create_provider<T: resources::Resource + Clone + PartialEq>( + &mut self, + initial_state: T, + ) -> Binding<T> { let type_id = initial_state.type_id(); - let providers = self.widget_providers.entry(type_id.clone()).or_insert(HashMap::default()); + let providers = self + .widget_providers + .entry(type_id.clone()) + .or_insert(HashMap::default()); if let Some(provider) = providers.get(&self.current_id) { if let Ok(state) = provider.get::<Binding<T>>() { @@ -129,7 +142,9 @@ impl KayakContext { /// Creates a context consumer for the given type, [T] /// /// This allows direct access to a parent's state data made with [create_provider](Self::create_provider). - pub fn create_consumer<T: resources::Resource + Clone + PartialEq>(&mut self) -> Option<Binding<T>> { + pub fn create_consumer<T: resources::Resource + Clone + PartialEq>( + &mut self, + ) -> Option<Binding<T>> { let type_id = std::any::TypeId::of::<T>(); if let Some(providers) = self.widget_providers.get(&type_id) { @@ -137,7 +152,7 @@ impl KayakContext { while index.is_some() { // Traverse the parents to find the one with the given state data index = self.widget_manager.tree.get_parent(index.unwrap()); - + let key = index.unwrap(); if let Some(provider) = providers.get(&key) { if let Ok(state) = provider.get::<Binding<T>>() { @@ -242,7 +257,11 @@ impl KayakContext { /// println!("Value: {}", my_state_clone.get()); /// }, &[&my_state]); /// ``` - pub fn create_effect<'a, F: Fn() + Send + Sync + 'static>(&'a mut self, effect: F, dependencies: &[&'a dyn Changeable]) { + pub fn create_effect<'a, F: Fn() + Send + Sync + 'static>( + &'a mut self, + effect: F, + dependencies: &[&'a dyn Changeable], + ) { // === Bind to Dependencies === // let notification = crate::notify(effect); let mut lifetimes = Vec::default(); @@ -262,9 +281,14 @@ impl KayakContext { }; // === Insert Effect === // - let effects = self.widget_effects.entry(self.current_id).or_insert(resources::Resources::default()); + let effects = self + .widget_effects + .entry(self.current_id) + .or_insert(resources::Resources::default()); if effects.contains::<MultiState<Vec<Box<dyn Releasable>>>>() { - let mut state = effects.get_mut::<MultiState<Vec<Box<dyn Releasable>>>>().unwrap(); + let mut state = effects + .get_mut::<MultiState<Vec<Box<dyn Releasable>>>>() + .unwrap(); let old_size = state.data.len(); state.get_or_add(lifetimes, &mut self.current_effect_index); if old_size != state.data.len() { @@ -290,7 +314,11 @@ impl KayakContext { } /// Create a `Releasable` lifetime that marks the current node as dirty when the given state changes - fn create_lifetime<T: resources::Resource + Clone + PartialEq>(state: &Binding<T>, widget_manager: &WidgetManager, id: Index) -> Box<dyn Releasable> { + fn create_lifetime<T: resources::Resource + Clone + PartialEq>( + state: &Binding<T>, + widget_manager: &WidgetManager, + id: Index, + ) -> Box<dyn Releasable> { let dirty_nodes = widget_manager.dirty_nodes.clone(); state.when_changed(crate::notify(move || { if let Ok(mut dirty_nodes) = dirty_nodes.lock() { @@ -302,10 +330,10 @@ impl KayakContext { fn insert_state_lifetime( lifetimes: &mut HashMap< crate::Index, - HashMap<flo_binding::Uuid, Box<dyn crate::Releasable>>, + HashMap<crate::flo_binding::Uuid, Box<dyn crate::Releasable>>, >, id: Index, - binding_id: flo_binding::Uuid, + binding_id: crate::flo_binding::Uuid, lifetime: Box<dyn crate::Releasable>, ) { if lifetimes.contains_key(&id) { @@ -324,10 +352,10 @@ impl KayakContext { fn remove_state_lifetime( lifetimes: &mut HashMap< crate::Index, - HashMap<flo_binding::Uuid, Box<dyn crate::Releasable>>, + HashMap<crate::flo_binding::Uuid, Box<dyn crate::Releasable>>, >, id: Index, - binding_id: flo_binding::Uuid, + binding_id: crate::flo_binding::Uuid, ) { if lifetimes.contains_key(&id) { if let Some(lifetimes) = lifetimes.get_mut(&id) { @@ -415,8 +443,8 @@ impl KayakContext { #[cfg(feature = "bevy_renderer")] pub fn query_world<T: bevy::ecs::system::SystemParam, F, R>(&mut self, mut f: F) -> R - where - F: FnMut(<T::Fetch as bevy::ecs::system::SystemParamFetch<'_, '_>>::Item) -> R, + where + F: FnMut(<T::Fetch as bevy::ecs::system::SystemParamFetch<'_, '_>>::Item) -> R, { let mut world = self.get_global_state::<bevy::prelude::World>().unwrap(); let mut system_state = bevy::ecs::system::SystemState::<T>::new(&mut world); @@ -428,4 +456,35 @@ impl KayakContext { r } + + pub fn get_asset<T: 'static + Send + Sync + Clone + PartialEq>( + &mut self, + key: impl Into<PathBuf>, + ) -> Binding<Option<T>> { + self.create_asset_storage::<T>(); + if let Ok(mut asset_storage) = self.assets.get_mut::<AssetStorage<T>>() { + asset_storage.get_asset(key).clone() + } else { + panic!("Couldn't find asset storage but it should exist!"); + } + } + + pub fn set_asset<T: 'static + Send + Sync + Clone + PartialEq>( + &mut self, + key: impl Into<PathBuf>, + asset: T, + ) { + self.create_asset_storage::<T>(); + if let Ok(mut asset_storage) = self.assets.get_mut::<AssetStorage<T>>() { + asset_storage.set_asset(key, asset); + } else { + panic!("Couldn't find asset storage but it should exist!"); + } + } + + fn create_asset_storage<T: 'static + Send + Sync + Clone + PartialEq>(&mut self) { + if !self.assets.contains::<AssetStorage<T>>() { + self.assets.insert(AssetStorage::<T>::new()); + } + } } diff --git a/kayak_core/src/flo_binding/CHANGELOG b/kayak_core/src/flo_binding/CHANGELOG new file mode 100644 index 0000000..de09838 --- /dev/null +++ b/kayak_core/src/flo_binding/CHANGELOG @@ -0,0 +1,6 @@ +A list of changes can be found here: +https://github.com/Logicalshift/flo_binding/compare/master...StarArawn:master + +Notable changes: +- Use send + sync for all types that just required Send before. +- Bindings are given ID's(UUID's) to track them. diff --git a/kayak_core/src/flo_binding/LICENSE b/kayak_core/src/flo_binding/LICENSE new file mode 100644 index 0000000..6b3388e --- /dev/null +++ b/kayak_core/src/flo_binding/LICENSE @@ -0,0 +1,203 @@ +This license is for the flo_binding crate: + +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/kayak_core/src/flo_binding/bind_stream.rs b/kayak_core/src/flo_binding/bind_stream.rs new file mode 100644 index 0000000..954bc6b --- /dev/null +++ b/kayak_core/src/flo_binding/bind_stream.rs @@ -0,0 +1,217 @@ +use super::traits::*; +use super::releasable::*; +use super::binding_context::*; + +use futures::prelude::*; +use ::desync::*; + +use std::sync::*; + +#[allow(dead_code)] +/// +/// Uses a stream to update a binding +/// +pub fn bind_stream<S, Value, UpdateFn>(stream: S, initial_value: Value, update: UpdateFn) -> StreamBinding<Value> +where S: 'static+Send+Stream+Unpin, + Value: 'static+Send+Clone+PartialEq, + UpdateFn: 'static+Send+FnMut(Value, S::Item) -> Value, + S::Item: Send { + // Create the content of the binding + let value = Arc::new(Mutex::new(initial_value)); + let core = StreamBindingCore { + value: Arc::clone(&value), + notifications: vec![] + }; + + let core = Arc::new(Desync::new(core)); + let mut update = update; + + // Send in the stream + pipe_in(Arc::clone(&core), stream, + move |core, next_item| { + // Only lock the value while updating it + let need_to_notify = { + // Update the value + let mut value = core.value.lock().unwrap(); + let new_value = update((*value).clone(), next_item); + + if new_value != *value { + // Update the value in the core + *value = new_value; + + // Notify anything that's listening + true + } else { + false + } + }; + + // If the update changed the value, then call the notifications (with the lock released, in case any try to read the value) + if need_to_notify { + core.notifications.retain(|notify| notify.is_in_use()); + core.notifications.iter().for_each(|notify| { notify.mark_as_changed(); }); + } + + Box::pin(future::ready(())) + }); + + StreamBinding { + core: core, + value: value + } +} + +/// +/// Binding that represents the result of binding a stream to a value +/// +#[derive(Clone)] +pub struct StreamBinding<Value: Send> { + /// The core of the binding (where updates are streamed and notifications sent) + core: Arc<Desync<StreamBindingCore<Value>>>, + + /// The current value of the binding + value: Arc<Mutex<Value>> +} + +/// +/// The data stored with a stream binding +/// +struct StreamBindingCore<Value: Send> { + /// The current value of this binidng + value: Arc<Mutex<Value>>, + + /// The items that should be notified when this binding changes + notifications: Vec<ReleasableNotifiable> +} + +impl<Value: 'static+Send+Clone> Bound<Value> for StreamBinding<Value> { + /// + /// Retrieves the value stored by this binding + /// + fn get(&self) -> Value { + BindingContext::add_dependency(self.clone()); + + let value = self.value.lock().unwrap(); + (*value).clone() + } +} + +impl<Value: 'static+Send> Changeable for StreamBinding<Value> { + /// + /// Supplies a function to be notified when this item is changed + /// + fn when_changed(&self, what: Arc<dyn Notifiable>) -> Box<dyn Releasable> { + // Create the notification object + let releasable = ReleasableNotifiable::new(what); + let notifiable = releasable.clone_as_owned(); + + // Send to the core + self.core.desync(move |core| { + core.notifications.push(notifiable); + }); + + // Return the releasable object + Box::new(releasable) + } +} + +#[cfg(test)] +mod test { + use super::*; + use super::super::notify_fn::*; + + use futures::stream; + use futures::executor; + use futures::channel::mpsc; + + use std::thread; + use std::time::Duration; + + #[test] + pub fn stream_in_all_values() { + // Stream with the values '1,2,3' + let stream = vec![1, 2, 3]; + let stream = stream::iter(stream.into_iter()); + + // Send the stream to a new binding + let binding = bind_stream(stream, 0, |_old_value, new_value| new_value); + + thread::sleep(Duration::from_millis(10)); + + // Binding should have the value of the last value in the stream + assert!(binding.get() == 3); + } + + #[test] + pub fn stream_processes_updates() { + // Stream with the values '1,2,3' + let stream = vec![1, 2, 3]; + let stream = stream::iter(stream.into_iter()); + + // Send the stream to a new binding (with some processing) + let binding = bind_stream(stream, 0, |_old_value, new_value| new_value + 42); + + thread::sleep(Duration::from_millis(10)); + + // Binding should have the value of the last value in the stream + assert!(binding.get() == 45); + } + + #[test] + pub fn notifies_on_change() { + // Create somewhere to send our notifications + let (mut sender, receiver) = mpsc::channel(0); + + // Send the receiver stream to a new binding + let binding = bind_stream(receiver, 0, |_old_value, new_value| new_value); + + // Create the notification + let notified = Arc::new(Mutex::new(false)); + let also_notified = Arc::clone(¬ified); + + binding.when_changed(notify(move || *also_notified.lock().unwrap() = true)).keep_alive(); + + // Should be initially un-notified + thread::sleep(Duration::from_millis(5)); + assert!(*notified.lock().unwrap() == false); + + executor::block_on(async { + // Send a value to the sender + sender.send(42).await.unwrap(); + + // Should get notified + thread::sleep(Duration::from_millis(5)); + assert!(*notified.lock().unwrap() == true); + assert!(binding.get() == 42); + }) + } + + #[test] + pub fn no_notification_on_no_change() { + // Create somewhere to send our notifications + let (mut sender, receiver) = mpsc::channel(0); + + // Send the receiver stream to a new binding + let binding = bind_stream(receiver, 0, |_old_value, new_value| new_value); + + // Create the notification + let notified = Arc::new(Mutex::new(false)); + let also_notified = Arc::clone(¬ified); + + binding.when_changed(notify(move || *also_notified.lock().unwrap() = true)).keep_alive(); + + // Should be initially un-notified + thread::sleep(Duration::from_millis(5)); + assert!(*notified.lock().unwrap() == false); + + executor::block_on(async { + // Send a value to the sender. This leaves the final value the same, so no notification should be generated. + sender.send(0).await.unwrap(); + + // Should not get notified + thread::sleep(Duration::from_millis(5)); + assert!(*notified.lock().unwrap() == false); + assert!(binding.get() == 0); + }); + } +} diff --git a/kayak_core/src/flo_binding/binding.rs b/kayak_core/src/flo_binding/binding.rs new file mode 100644 index 0000000..1d719e1 --- /dev/null +++ b/kayak_core/src/flo_binding/binding.rs @@ -0,0 +1,237 @@ +use super::traits::*; +use super::releasable::*; +use super::binding_context::*; + +use std::sync::*; +pub use uuid::Uuid; + +/// +/// An internal representation of a bound value +/// +struct BoundValue<Value> { + /// The current value of this binding + value: Value, + + /// What to call when the value changes + when_changed: Vec<ReleasableNotifiable> +} + +impl<Value: Clone+PartialEq> BoundValue<Value> { + /// + /// Creates a new binding with the specified value + /// + pub fn new(val: Value) -> BoundValue<Value> { + BoundValue { + value: val, + when_changed: vec![] + } + } + + /// + /// Updates the value in this structure without calling the notifications, returns whether or not anything actually changed + /// + pub fn set_without_notifying(&mut self, new_value: Value) -> bool { + let changed = self.value != new_value; + + self.value = new_value; + + changed + } + + /// + /// Retrieves a copy of the list of notifiable items for this value + /// + pub fn get_notifiable_items(&self) -> Vec<ReleasableNotifiable> { + self.when_changed + .iter() + .map(|item| item.clone_for_inspection()) + .collect() + } + + /// + /// If there are any notifiables in this object that aren't in use, remove them + /// + pub fn filter_unused_notifications(&mut self) { + self.when_changed.retain(|releasable| releasable.is_in_use()); + } + + /// + /// Retrieves the value of this item + /// + fn get(&self) -> Value { + self.value.clone() + } + + /// + /// Retrieves a mutable reference to the value of this item + /// + fn get_mut(&mut self) -> &mut Value { + &mut self.value + } + + /// + /// Adds something that will be notified when this item changes + /// + fn when_changed(&mut self, what: Arc<dyn Notifiable>) -> Box<dyn Releasable> { + let releasable = ReleasableNotifiable::new(what); + self.when_changed.push(releasable.clone_as_owned()); + + self.filter_unused_notifications(); + + Box::new(releasable) + } +} + +impl<Value: Default + Clone + PartialEq> Default for BoundValue<Value> { + fn default() -> Self { + BoundValue::new(Value::default()) + } +} + +impl<Value: std::fmt::Debug> std::fmt::Debug for BoundValue<Value> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.value.fmt(f) + } +} + +impl<Value: PartialEq> PartialEq for BoundValue<Value> { + fn eq(&self, other: &Self) -> bool { + self.value.eq(&other.value) + } +} +/// +/// Represents a thread-safe, sharable binding +/// +#[derive(Clone)] +pub struct Binding<Value> { + pub id: Uuid, + /// The value stored in this binding + value: Arc<Mutex<BoundValue<Value>>> +} + +impl<Value: Default + Clone + PartialEq> Default for Binding<Value> { + fn default() -> Self { + Binding::new(Value::default()) + } +} + +impl<Value: std::fmt::Debug> std::fmt::Debug for Binding<Value> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.value.fmt(f) + } +} + +impl<Value: PartialEq> PartialEq for Binding<Value> { + fn eq(&self, other: &Self) -> bool { + self.value.lock().unwrap().eq(&other.value.lock().unwrap()) + } +} + +impl<Value: Clone+PartialEq> Binding<Value> { + pub fn new(value: Value) -> Binding<Value> { + Binding { + id: Uuid::new_v4(), + value: Arc::new(Mutex::new(BoundValue::new(value))) + } + } +} + +impl<Value: 'static+Clone+PartialEq+Send+Sync> Changeable for Binding<Value> { + fn when_changed(&self, what: Arc<dyn Notifiable>) -> Box<dyn Releasable> { + self.value.lock().unwrap().when_changed(what) + } +} + +impl<Value: 'static+Clone+PartialEq+Send+Sync> Bound<Value> for Binding<Value> { + fn get(&self) -> Value { + BindingContext::add_dependency(self.clone()); + + self.value.lock().unwrap().get() + } +} + +impl<Value: 'static+Clone+PartialEq+Send+Sync> MutableBound<Value> for Binding<Value> { + fn set(&self, new_value: Value) { + // Update the value with the lock held + let notifications = { + let mut cell = self.value.lock().unwrap(); + let changed = cell.set_without_notifying(new_value); + + if changed { + cell.get_notifiable_items() + } else { + vec![] + } + }; + + // Call the notifications outside of the lock + let mut needs_filtering = false; + + for to_notify in notifications { + needs_filtering = !to_notify.mark_as_changed() || needs_filtering; + } + + if needs_filtering { + let mut cell = self.value.lock().unwrap(); + cell.filter_unused_notifications(); + } + } +} + +impl<Value: 'static + Clone + PartialEq + Send + Sync> WithBound<Value> for Binding<Value> { + fn with_ref<F, T>(&self, f: F) -> T + where + F: FnOnce(&Value) -> T, + { + f(&self.value.lock().unwrap().value) + } + fn with_mut<F>(&self, f: F) + where + F: FnOnce(&mut Value) -> bool, + { + let notifications = { + let mut v = self.value.lock().unwrap(); + let changed = f(v.get_mut()); + + if changed { + v.get_notifiable_items() + } else { + vec![] + } + }; + + // Call the notifications outside of the lock + let mut needs_filtering = false; + + for to_notify in notifications { + needs_filtering = !to_notify.mark_as_changed() || needs_filtering; + } + + if needs_filtering { + let mut cell = self.value.lock().unwrap(); + cell.filter_unused_notifications(); + } + } + +} + +impl<Value: 'static+Clone+PartialEq+Send+Sync> From<Value> for Binding<Value> { + #[inline] + fn from(val: Value) -> Binding<Value> { + Binding::new(val) + } +} + +impl<'a, Value: 'static+Clone+PartialEq+Send+Sync> From<&'a Binding<Value>> for Binding<Value> { + #[inline] + fn from(val: &'a Binding<Value>) -> Binding<Value> { + Binding::clone(val) + } +} + +impl<'a, Value: 'static+Clone+PartialEq+Send+Sync> From<&'a Value> for Binding<Value> { + #[inline] + fn from(val: &'a Value) -> Binding<Value> { + Binding::new(val.clone()) + } +} diff --git a/kayak_core/src/flo_binding/binding_context.rs b/kayak_core/src/flo_binding/binding_context.rs new file mode 100644 index 0000000..776bd51 --- /dev/null +++ b/kayak_core/src/flo_binding/binding_context.rs @@ -0,0 +1,181 @@ +use super::traits::*; +use super::notify_fn::*; + +use std::rc::*; +use std::sync::*; +use std::cell::*; + +thread_local! { + static CURRENT_CONTEXT: RefCell<Option<BindingContext>> = RefCell::new(None); +} + +/// +/// Represents the dependencies of a binding context +/// +#[derive(Clone)] +pub struct BindingDependencies { + /// Set to true if the binding dependencies have been changed since they were registered in the dependencies + recently_changed: Arc<Mutex<bool>>, + + /// The when_changed monitors for the recently_changed flag + recent_change_monitors: Rc<RefCell<Vec<Box<dyn Releasable>>>>, + + /// The list of changables that are dependent on this context + dependencies: Rc<RefCell<Vec<Box<dyn Changeable>>>> +} + +impl BindingDependencies { + /// + /// Creates a new binding dependencies object + /// + pub fn new() -> BindingDependencies { + BindingDependencies { + recently_changed: Arc::new(Mutex::new(false)), + recent_change_monitors: Rc::new(RefCell::new(vec![])), + dependencies: Rc::new(RefCell::new(vec![])) + } + } + + /// + /// Adds a new dependency to this object + /// + pub fn add_dependency<TChangeable: Changeable+'static>(&mut self, dependency: TChangeable) { + // Set the recently changed flag so that we can tell if the dependencies are already out of date before when_changed is called + let recently_changed = Arc::clone(&self.recently_changed); + let mut recent_change_monitors = self.recent_change_monitors.borrow_mut(); + recent_change_monitors.push(dependency.when_changed(notify(move || { *recently_changed.lock().unwrap() = true; }))); + + // Add this dependency to the list + self.dependencies.borrow_mut().push(Box::new(dependency)) + } + + /// + /// If the dependencies have not changed since they were registered, registers for changes + /// and returns a `Releasable`. If the dependencies are already different, returns `None`. + /// + pub fn when_changed_if_unchanged(&self, what: Arc<dyn Notifiable>) -> Option<Box<dyn Releasable>> { + let mut to_release = vec![]; + + // Register with all of the dependencies + for dep in self.dependencies.borrow_mut().iter_mut() { + to_release.push(dep.when_changed(Arc::clone(&what))); + } + + if *self.recently_changed.lock().unwrap() { + // If a value changed while we were building these dependencies, then immediately generate the notification + to_release.into_iter().for_each(|mut releasable| releasable.done()); + + // Nothing to release + None + } else { + // Otherwise, return the set of releasable values + Some(Box::new(to_release)) + } + } +} + +impl Changeable for BindingDependencies { + fn when_changed(&self, what: Arc<dyn Notifiable>) -> Box<dyn Releasable> { + let when_changed_or_not = self.when_changed_if_unchanged(Arc::clone(&what)); + + match when_changed_or_not { + Some(releasable) => releasable, + None => { + what.mark_as_changed(); + Box::new(vec![]) + } + } + } +} + +/// +/// Represents a binding context. Binding contexts are +/// per-thread structures, used to track +/// +#[derive(Clone)] +pub struct BindingContext { + /// The dependencies for this context + dependencies: BindingDependencies, + + /// None, or the binding context that this context was created within + nested: Option<Box<BindingContext>> +} + +impl BindingContext { + /// + /// Gets the active binding context + /// + pub fn current() -> Option<BindingContext> { + CURRENT_CONTEXT.with(|current_context| { + current_context + .borrow() + .as_ref() + .cloned() + }) + } + + /// + /// Panics if we're trying to create a binding, with a particular message + /// + pub fn panic_if_in_binding_context(msg: &str) { + if CURRENT_CONTEXT.with(|context| context.borrow().is_some()) { + panic!("Not possible when binding: {}", msg); + } + } + + /// + /// Executes a function in a new binding context + /// + pub fn bind<TResult, TFn>(to_do: TFn) -> (TResult, BindingDependencies) + where TFn: FnOnce() -> TResult { + // Remember the previous context + let previous_context = Self::current(); + + // Create a new context + let dependencies = BindingDependencies::new(); + let new_context = BindingContext { + dependencies: dependencies.clone(), + nested: previous_context.clone().map(Box::new) + }; + + // Make the current context the same as the new context + CURRENT_CONTEXT.with(|current_context| *current_context.borrow_mut() = Some(new_context)); + + // Perform the requested action with this context + let result = to_do(); + + // Reset to the previous context + CURRENT_CONTEXT.with(|current_context| *current_context.borrow_mut() = previous_context); + + (result, dependencies) + } + + #[allow(dead_code)] + /// + /// Performs an action outside of the binding context (dependencies + /// will not be tracked for anything the supplied function does) + /// + pub fn out_of_context<TResult, TFn>(to_do: TFn) -> TResult + where TFn: FnOnce() -> TResult { + // Remember the previous context + let previous_context = Self::current(); + + // Unset the context + CURRENT_CONTEXT.with(|current_context| *current_context.borrow_mut() = None); + + // Perform the operations without a binding context + let result = to_do(); + + // Reset to the previous context + CURRENT_CONTEXT.with(|current_context| *current_context.borrow_mut() = previous_context); + + result + } + + /// + /// Adds a dependency to the current context (if one is found) + /// + pub fn add_dependency<TChangeable: Changeable+'static>(dependency: TChangeable) { + Self::current().map(|mut ctx| ctx.dependencies.add_dependency(dependency)); + } +} diff --git a/kayak_core/src/flo_binding/bindref.rs b/kayak_core/src/flo_binding/bindref.rs new file mode 100644 index 0000000..ec4fa57 --- /dev/null +++ b/kayak_core/src/flo_binding/bindref.rs @@ -0,0 +1,183 @@ +#[cfg(feature = "stream")] +use super::bind_stream::*; +use super::binding::*; +use super::computed::*; +use super::traits::*; + +use std::sync::*; + +/// +/// A `BindRef` references another binding without needing to know precisely +/// what kind of binding it is. It is read-only, so mostly useful for passing +/// a binding around, particularly for computed bindings. Create one with +/// `BindRef::from(binding)`. +/// +/// Cloning a `BindRef` will create another reference to the same binding. +/// +pub struct BindRef<Target> { + reference: Arc<dyn Bound<Target>>, +} + +impl<Value> Bound<Value> for BindRef<Value> { + #[inline] + fn get(&self) -> Value { + self.reference.get() + } +} + +impl<Value> Changeable for BindRef<Value> { + #[inline] + fn when_changed(&self, what: Arc<dyn Notifiable>) -> Box<dyn Releasable> { + self.reference.when_changed(what) + } +} + +impl<Value> Clone for BindRef<Value> { + fn clone(&self) -> Self { + BindRef { + reference: Arc::clone(&self.reference), + } + } +} + +impl<Value> BindRef<Value> { + /// + /// Creates a new BindRef from a reference to an existing binding + /// + #[inline] + #[allow(dead_code)] + pub fn new<Binding: 'static + Clone + Bound<Value>>(binding: &Binding) -> BindRef<Value> { + BindRef { + reference: Arc::new(binding.clone()), + } + } + + /// + /// Creates a new BindRef from an existing binding + /// + #[inline] + #[allow(dead_code)] + pub fn from_arc<Binding: 'static + Bound<Value>>(binding_ref: Arc<Binding>) -> BindRef<Value> { + BindRef { + reference: binding_ref, + } + } +} + +impl<'a, Value> From<&'a BindRef<Value>> for BindRef<Value> { + #[inline] + fn from(val: &'a BindRef<Value>) -> Self { + BindRef::clone(val) + } +} + +impl<Value: 'static + Clone + Send + Sync + PartialEq> From<Binding<Value>> for BindRef<Value> { + #[inline] + fn from(val: Binding<Value>) -> Self { + BindRef { + reference: Arc::new(val), + } + } +} + +impl<'a, Value: 'static + Clone + PartialEq + Sync + Send> From<&'a Binding<Value>> + for BindRef<Value> +{ + #[inline] + fn from(val: &'a Binding<Value>) -> Self { + BindRef { + reference: Arc::new(val.clone()), + } + } +} + +impl<Value: 'static + Clone + PartialEq + Sync + Send, TFn> From<ComputedBinding<Value, TFn>> + for BindRef<Value> +where + TFn: 'static + Send + Sync + Fn() -> Value, +{ + #[inline] + fn from(val: ComputedBinding<Value, TFn>) -> Self { + BindRef { + reference: Arc::new(val), + } + } +} + +impl<'a, Value: 'static + Clone + PartialEq + Sync + Send, TFn> + From<&'a ComputedBinding<Value, TFn>> for BindRef<Value> +where + TFn: 'static + Send + Sync + Fn() -> Value, +{ + #[inline] + fn from(val: &'a ComputedBinding<Value, TFn>) -> Self { + BindRef { + reference: Arc::new(val.clone()), + } + } +} + +impl<'a, Value: 'static + Clone + PartialEq + Send + Sync + Into<Binding<Value>>> From<&'a Value> + for BindRef<Value> +{ + #[inline] + fn from(val: &'a Value) -> BindRef<Value> { + let binding: Binding<Value> = val.into(); + BindRef::from(binding) + } +} + +#[cfg(feature = "stream")] +impl<Value: 'static + Clone + Send + Sync + PartialEq> From<StreamBinding<Value>> + for BindRef<Value> +{ + #[inline] + fn from(val: StreamBinding<Value>) -> Self { + BindRef { + reference: Arc::new(val), + } + } +} + +#[cfg(test)] +mod test { + use super::super::*; + + #[test] + fn bindref_matches_core_value() { + let bind = bind(1); + let bind_ref = BindRef::from(bind.clone()); + + assert!(bind_ref.get() == 1); + + bind.set(2); + + assert!(bind_ref.get() == 2); + } + + #[test] + fn bind_ref_from_value() { + let bind_ref = BindRef::from(&1); + + assert!(bind_ref.get() == 1); + } + + #[test] + fn bind_ref_from_computed() { + let bind_ref = BindRef::from(computed(|| 1)); + + assert!(bind_ref.get() == 1); + } + + #[test] + fn bindref_matches_core_value_when_created_from_ref() { + let bind = bind(1); + let bind_ref = BindRef::new(&bind); + + assert!(bind_ref.get() == 1); + + bind.set(2); + + assert!(bind_ref.get() == 2); + } +} diff --git a/kayak_core/src/flo_binding/computed.rs b/kayak_core/src/flo_binding/computed.rs new file mode 100644 index 0000000..ae33a82 --- /dev/null +++ b/kayak_core/src/flo_binding/computed.rs @@ -0,0 +1,320 @@ +use super::traits::*; +use super::releasable::*; +use super::binding_context::*; + +use std::sync::*; +use std::mem; + +/// +/// Represents a computed value +/// +#[derive(Clone)] +enum ComputedValue<Value: 'static+Clone> { + Unknown, + Cached(Value) +} + +use self::ComputedValue::*; + +/// +/// Core representation ofa computed binding +/// +struct ComputedBindingCore<Value: 'static+Clone, TFn> +where TFn: 'static+Fn() -> Value { + /// Function to call to recalculate this item + calculate_value: TFn, + + /// Most recent cached value + latest_value: ComputedValue<Value>, + + /// If there's a notification attached to this item, this can be used to release it + existing_notification: Option<Box<dyn Releasable>>, + + /// What to call when the value changes + when_changed: Vec<ReleasableNotifiable> +} + +impl<Value: 'static+Clone, TFn> ComputedBindingCore<Value, TFn> +where TFn: 'static+Fn() -> Value { + /// + /// Creates a new computed binding core item + /// + pub fn new(calculate_value: TFn) -> ComputedBindingCore<Value, TFn> { + ComputedBindingCore { + calculate_value: calculate_value, + latest_value: Unknown, + existing_notification: None, + when_changed: vec![] + } + } + + /// + /// Marks the value as changed, returning true if the value was removed + /// + pub fn mark_changed(&mut self) -> bool { + match self.latest_value { + Unknown => false, + _ => { + self.latest_value = Unknown; + true + } + } + } + + /// + /// Retrieves a copy of the list of notifiable items for this value + /// + pub fn get_notifiable_items(&self) -> Vec<ReleasableNotifiable> { + self.when_changed.iter() + .map(|item| item.clone_for_inspection()) + .collect() + } + + /// + /// If there are any notifiables in this object that aren't in use, remove them + /// + pub fn filter_unused_notifications(&mut self) { + self.when_changed.retain(|releasable| releasable.is_in_use()); + } + + /// + /// Returns the current value (or 'Unknown' if it needs recalculating) + /// + pub fn get(&self) -> ComputedValue<Value> { + self.latest_value.clone() + } + + /// + /// Recalculates the latest value + /// + pub fn recalculate(&mut self) -> (Value, BindingDependencies) { + // Perform the binding in a context to get the value and the dependencies + let (result, dependencies) = BindingContext::bind(|| (self.calculate_value)()); + + // Update the latest value + self.latest_value = Cached(result.clone()); + + // Pass on the result + (result, dependencies) + } +} + +impl<Value: 'static+Clone, TFn> Drop for ComputedBindingCore<Value, TFn> +where TFn: 'static+Fn() -> Value { + fn drop(&mut self) { + // No point receiving any notifications once the core has gone + // (The notification can still fire if it has a weak reference) + if let Some(ref mut existing_notification) = self.existing_notification { + existing_notification.done() + } + } +} + +/// +/// Represents a binding to a value that is computed by a function +/// +pub struct ComputedBinding<Value: 'static+Clone, TFn> +where TFn: 'static+Fn() -> Value { + /// The core where the binding data is stored + core: Arc<Mutex<ComputedBindingCore<Value, TFn>>> +} + +impl<Value: 'static+Clone+Send, TFn> ComputedBinding<Value, TFn> +where TFn: 'static+Send+Sync+Fn() -> Value { + /// + /// Creates a new computable binding + /// + pub fn new(calculate_value: TFn) -> ComputedBinding<Value, TFn> { + // Computed bindings created in a binding context will likely not be + // retained, so things won't update as you expect. + // + // We could add some special logic to retain them here, or we could + // just panic. Panicking is probably better as really what should be + // done is to evaluate the content of the computed value directly. + // This can happen if we call a function that returns a binding and + // it creates one rather than returning an existing one. + BindingContext::panic_if_in_binding_context("Cannot create computed bindings in a computed value calculation function (you should evaluate the value directly rather than create bindings)"); + + // Create the binding + ComputedBinding { + core: Arc::new(Mutex::new(ComputedBindingCore::new(calculate_value))) + } + } + + /// + /// Creates a new computable binding within another binding + /// + /// Normally this is considered an error (if the binding is not held anywhere + /// outside of the context, it will never generate an update). `new` panics + /// if it's called from within a context for this reason. + /// + /// If the purpose of a computed binding is to return other bindings, this + /// limitation does not apply, so this call is available + /// + pub fn new_in_context(calculate_value: TFn) -> ComputedBinding<Value, TFn> { + // Create the binding + ComputedBinding { + core: Arc::new(Mutex::new(ComputedBindingCore::new(calculate_value))) + } + } + + /// + /// Marks this computed binding as having changed + /// + fn mark_changed(&self, force_notify: bool) { + // We do the notifications and releasing while the lock is not retained + let (notifiable, releasable) = { + // Get the core + let mut core = self.core.lock().unwrap(); + + // Mark it as changed + let actually_changed = core.mark_changed() || force_notify; + + core.filter_unused_notifications(); + + // Get the items that need changing (once we've notified our dependencies that we're changed, we don't need to notify them again until we get recalculated) + let notifiable = if actually_changed { + core.get_notifiable_items() + } else { + vec![] + }; + + // Extract the releasable so we can release it after the lock has gone + let mut releasable: Option<Box<dyn Releasable>> = None; + mem::swap(&mut releasable, &mut core.existing_notification); + + // These values are needed outside of the lock + (notifiable, releasable) + }; + + // Don't want any more notifications from this source + releasable.map(|mut releasable| releasable.done()); + + // Notify anything that needs to be notified that this has changed + for to_notify in notifiable { + to_notify.mark_as_changed(); + } + } + + /// + /// Mark this item as changed whenever 'to_monitor' is changed + /// Core should already be locked, returns true if the value is already changed and we should immediately notify + /// + fn monitor_changes(&self, core: &mut ComputedBindingCore<Value, TFn>, to_monitor: &mut BindingDependencies) -> bool { + // We only keep a weak reference to the core here + let to_notify = Arc::downgrade(&self.core); + + // Monitor for changes (see below for the implementation against to_notify's type) + let lifetime = to_monitor.when_changed_if_unchanged(Arc::new(to_notify)); + let already_changed = lifetime.is_none(); + + // Store the lifetime + let mut last_notification = lifetime; + mem::swap(&mut last_notification, &mut core.existing_notification); + + // Any lifetime that was in the core before this one should be finished + last_notification.map(|mut last_notification| last_notification.done()); + + // Return if the value is already changed + already_changed + } +} + +/// +/// The weak reference to a core is generated in `monitor_changes`: this specifies what happens when a +/// notification is generated for such a reference. +/// +impl<Value, TFn> Notifiable for Weak<Mutex<ComputedBindingCore<Value, TFn>>> +where Value: 'static+Clone+Send, TFn: 'static+Send+Sync+Fn() -> Value { + fn mark_as_changed(&self) { + // If the reference is still active, reconstitute a computed binding in order to call the mark_changed method + if let Some(to_notify) = self.upgrade() { + let to_notify = ComputedBinding { core: to_notify }; + to_notify.mark_changed(false); + } else if cfg!(debug_assertions) { + // We can carry on here, but this suggests a memory leak - if the core has gone, then its owning object should have stopped this event from firing + panic!("The core of a computed is gone but its notifcations have been left behind"); + } + } +} + +impl<Value: 'static+Clone+Send, TFn> Clone for ComputedBinding<Value, TFn> +where TFn: 'static+Send+Sync+Fn() -> Value { + fn clone(&self) -> Self { + ComputedBinding { core: Arc::clone(&self.core) } + } +} + +impl<Value: 'static+Clone, TFn> Changeable for ComputedBinding<Value, TFn> +where TFn: 'static+Send+Sync+Fn() -> Value { + fn when_changed(&self, what: Arc<dyn Notifiable>) -> Box<dyn Releasable> { + let releasable = ReleasableNotifiable::new(what); + + // Lock the core and push this as a thing to perform when this value changes + let mut core = self.core.lock().unwrap(); + core.when_changed.push(releasable.clone_as_owned()); + + core.filter_unused_notifications(); + + Box::new(releasable) + } +} + +impl<Value: 'static+Clone+Send, TFn> Bound<Value> for ComputedBinding<Value, TFn> +where TFn: 'static+Send+Sync+Fn() -> Value { + fn get(&self) -> Value { + // This is a dependency of the current binding context + BindingContext::add_dependency(self.clone()); + + // Set to true if the value changes while we're reading it + // (presumably because it's updating rapidly) + let mut notify_immediately = false; + let result; + + { + // Borrow the core + let mut core = self.core.lock().unwrap(); + + if let Cached(value) = core.get() { + // The value already exists in this item + result = value; + } else { + // TODO: really want to recalculate without locking the core - can do this by moving the function out and doing the recalculation here + // TODO: locking the core and calling a function can result in deadlocks due to user code structure in particular against other bindings + // TODO: when we do recalculate without locking, we need to make sure that no extra invalidations arrived between when we started the calculation and when we stored the result + // TODO: if multiple calculations do occur outside the lock, we need to return only the most recent result so when_changed is fired correctly + + // Stop responding to notifications + let mut old_notification = None; + mem::swap(&mut old_notification, &mut core.existing_notification); + + if let Some(mut last_notification) = old_notification { + last_notification.done(); + } + + // Need to re-calculate the core + let (value, mut dependencies) = core.recalculate(); + + // If any of the dependencies change, mark this item as changed too + notify_immediately = self.monitor_changes(&mut core, &mut dependencies); + + // If we're going to notify, unset the value we've cached + if notify_immediately { + core.latest_value = ComputedValue::Unknown; + } + + // TODO: also need to make sure that any hooks we have are removed if we're only referenced via a hook + + // Return the value + result = value; + } + } + + // If there was a change while we were calculating the value, generate a notification + if notify_immediately { + self.mark_changed(true); + } + + result + } +} diff --git a/kayak_core/src/flo_binding/follow.rs b/kayak_core/src/flo_binding/follow.rs new file mode 100644 index 0000000..7c82ce0 --- /dev/null +++ b/kayak_core/src/flo_binding/follow.rs @@ -0,0 +1,249 @@ +use super::notify_fn::*; +use super::traits::*; + +use futures::task; +use futures::task::Poll; +use futures::*; + +use ::desync::*; + +use std::marker::PhantomData; +use std::pin::Pin; +use std::sync::*; + +/// +/// The state of the binding for a follow stream +/// +#[derive(Copy, Clone)] +#[allow(dead_code)] +enum FollowState { + Unchanged, + Changed, +} + +/// +/// Core data structures for a follow stream +/// +struct FollowCore<TValue, Binding: Bound<TValue>> { + /// Changed if the binding value has changed, or Unchanged if it is not changed + state: FollowState, + + /// What to notify when this item is changed + notify: Option<task::Waker>, + + /// The binding that this is following + binding: Arc<Binding>, + + /// Value is stored in the binding + value: PhantomData<TValue>, +} + +/// +/// Stream that follows the values of a binding +/// +pub struct FollowStream<TValue: Send + Unpin, Binding: Bound<TValue>> { + /// The core of this future + core: Arc<Desync<FollowCore<TValue, Binding>>>, + + /// Lifetime of the watcher + _watcher: Box<dyn Releasable>, +} + +impl<TValue: 'static + Send + Unpin, Binding: 'static + Bound<TValue>> Stream + for FollowStream<TValue, Binding> +{ + type Item = TValue; + + fn poll_next(self: Pin<&mut Self>, cx: &mut task::Context) -> Poll<Option<Self::Item>> { + // If the core is in a 'changed' state, return the binding so we can fetch it + // Want to fetch the binding value outside of the lock as it can potentially change during calculation + let binding = self.core.sync(|core| { + match core.state { + FollowState::Unchanged => { + // Wake this future when changed + core.notify = Some(cx.waker().clone()); + None + } + + FollowState::Changed => { + // Value has changed since we were last notified: return the changed value + core.state = FollowState::Unchanged; + Some(Arc::clone(&core.binding)) + } + } + }); + + if let Some(binding) = binding { + Poll::Ready(Some(binding.get())) + } else { + Poll::Pending + } + } +} + +/// +/// Creates a stream from a binding +/// +#[allow(dead_code)] +pub fn follow<TValue: 'static + Send + Unpin, Binding: 'static + Bound<TValue>>( + binding: Binding, +) -> FollowStream<TValue, Binding> { + // Generate the initial core + let core = FollowCore { + state: FollowState::Changed, + notify: None, + binding: Arc::new(binding), + value: PhantomData, + }; + + // Notify whenever the binding changes + let core = Arc::new(Desync::new(core)); + let weak_core = Arc::downgrade(&core); + let watcher = core.sync(move |core| { + core.binding.when_changed(notify(move || { + if let Some(core) = weak_core.upgrade() { + let task = core.sync(|core| { + core.state = FollowState::Changed; + core.notify.take() + }); + task.map(|task| task.wake()); + } + })) + }); + + // Create the stream + FollowStream { + core: core, + _watcher: watcher, + } +} + +#[cfg(test)] +mod test { + use super::super::*; + use super::*; + + use futures::executor; + use futures::task::{waker_ref, ArcWake, Context}; + + use std::thread; + use std::time::Duration; + + struct NotifyNothing; + impl ArcWake for NotifyNothing { + fn wake_by_ref(_arc_self: &Arc<Self>) { + // zzz + } + } + + #[test] + fn follow_stream_has_initial_value() { + let binding = bind(1); + let bind_ref = BindRef::from(binding.clone()); + let mut stream = follow(bind_ref); + + executor::block_on(async { + assert!(stream.next().await == Some(1)); + }); + } + + #[test] + fn follow_stream_updates() { + let binding = bind(1); + let bind_ref = BindRef::from(binding.clone()); + let mut stream = follow(bind_ref); + + executor::block_on(async { + assert!(stream.next().await == Some(1)); + binding.set(2); + assert!(stream.next().await == Some(2)); + }); + } + + #[test] + fn computed_updates_during_read() { + // Computed value that takes a while to calculate (so we can always 'lose' the race between reading the value and starting a new update) + let binding = bind(1); + let bind_ref = BindRef::from(binding.clone()); + let computed = computed(move || { + let val = bind_ref.get(); + thread::sleep(Duration::from_millis(300)); + val + }); + let mut stream = follow(computed); + + // Read from the stream in the background + let reader = Desync::new(vec![]); + let read_values = reader.after( + async move { + let result = vec![stream.next().await, stream.next().await]; + result + }, + |val, read_val| { + *val = read_val; + }, + ); + + // Short delay so the reader starts + thread::sleep(Duration::from_millis(10)); + + // Update the binding + binding.set(2); + + // Wait for the values to be read from the stream + let values_read_from_stream = reader.sync(|val| val.clone()); + + // First read should return '1' + assert!(values_read_from_stream[0] == Some(1)); + + // Second read should return '2' + assert!(values_read_from_stream[1] == Some(2)); + + // Finish the read_values future + executor::block_on(read_values).unwrap(); + } + + #[test] + fn stream_is_unready_after_first_read() { + let binding = bind(1); + let bind_ref = BindRef::from(binding.clone()); + let waker = Arc::new(NotifyNothing); + let waker = waker_ref(&waker); + let mut context = Context::from_waker(&waker); + let mut stream = follow(bind_ref); + + assert!(stream.poll_next_unpin(&mut context) == Poll::Ready(Some(1))); + assert!(stream.poll_next_unpin(&mut context) == Poll::Pending); + } + + #[test] + fn stream_is_immediately_ready_after_write() { + let binding = bind(1); + let bind_ref = BindRef::from(binding.clone()); + let waker = Arc::new(NotifyNothing); + let waker = waker_ref(&waker); + let mut context = Context::from_waker(&waker); + let mut stream = follow(bind_ref); + + assert!(stream.poll_next_unpin(&mut context) == Poll::Ready(Some(1))); + binding.set(2); + assert!(stream.poll_next_unpin(&mut context) == Poll::Ready(Some(2))); + } + + #[test] + fn will_wake_when_binding_is_updated() { + let binding = bind(1); + let bind_ref = BindRef::from(binding.clone()); + let mut stream = follow(bind_ref); + + thread::spawn(move || { + thread::sleep(Duration::from_millis(100)); + binding.set(2); + }); + + executor::block_on(async { + assert!(stream.next().await == Some(1)); + assert!(stream.next().await == Some(2)); + }) + } +} diff --git a/kayak_core/src/flo_binding/mod.rs b/kayak_core/src/flo_binding/mod.rs new file mode 100644 index 0000000..20733f6 --- /dev/null +++ b/kayak_core/src/flo_binding/mod.rs @@ -0,0 +1,736 @@ +//! +//! # `flo_binding`, a data-driven binding library +//! +//! `flo_binding` is a library of types intended to help store state in interactive +//! applications. A binding is a mutable value that can notify observers when it +//! changes, or can supply its values as a stream. This is intended to make it easy +//! to build 'data-driven' applications, where updates to model state automatically +//! drive updates to the rest of the application. +//! +//! This method of propagating state is sometimes called 'reactive programming', +//! and has also been called 'parametric programming' in the past: it's most notable +//! for being the methodology behind spreadsheets and CAD applications before its +//! recent rediscovery in web applications. `flo_binding` prefers the term +//! 'data-driven' as it's a more plain description of how changes are pushed through +//! an application using the library. +//! +//! The advantage of using a binding library like this over a standard event-driven +//! architecture is that it's not necessary to send events or manually update state +//! when updating the application data model. Bugs are reduced because it's very +//! much harder for a dependent state update to go missing as this library can +//! automatically associate. +//! +//! There are three main types of binding provided by the `flo_binding` library: +//! +//! A normal binding, created by `bind(value)` can be updated to a new value by +//! calling `set()`. These are useful for representing state that is directly +//! manipulated by the user. +//! +//! A computed binding, created by `compute(move || { some_func() })`. When a +//! computed binding accesses the value of another binding by calling `get()`, it +//! will automatically monitor it for changes and indicate that it has changed +//! whenever that value changes. This makes it easy to create chains of dependent +//! bindings. +//! +//! A stream binding, created by `bind_stream(stream, initial_value, |old_value, new_value| { /* ... */ });`. +//! This makes it possible for a binding to update its values in response to a +//! stream of events, such as might be available from a UI framework. +//! +//! To make using computed bindings easier to generate, cloning a binding creates +//! a reference to the same piece of state. ie: `let a = bind(1); let b = a.clone();` +//! will give a and b a reference to the same value, so `a.set(2); b.get()` will +//! return 2. +//! +//! ## Event-driven architecture +//! +//! The `when_changed()` function can be called to register a function that is called +//! whenever a binding's value becomes invalidated. Once invalidated, a binding +//! remains invalidated until it's read with `get()`, which avoids computing values +//! for computed bindings that are never used. This can be used to integrate with +//! traditional UI frameworks. +//! +//! Calling `when_changed()` will return a lifetime object: the event will stop firing +//! when this object is dropped (or when `done()` is called on it). `keep_alive()` can +//! be used to keep the event firing for as long as the binding exists if necessary. +//! +//! The event-driven approach to application design has many disadvantages: the need to +//! manage event lifetimes is one, and another important one is the need to pass the +//! actual bindings around in order to attach events to them, so `flo_binding` provides +//! an alternative approach. +//! +//! ## Stream-driven architecture +//! +//! A superior alternative to the traditional OO-style event-driven architecture is +//! to take a stream-driven approach. Unlike events, streams manage their own lifetimes +//! and can be combined or split, and it's possible to pass around a stream without also +//! providing access to its source, which is a property that can be exploited to produce +//! a less interdependent application. FlowBetween, the application that flo_binding was +//! developed for is an example of a stream-driven application. +//! +//! Calling `follow(binding)` will generate a stream of updates from a binding. This +//! can be used to update part of a user interface directly. It can also be used as +//! an input for `bind_stream()` to create more complicated update rules than are +//! allowed by a simple computed binding. +//! +//! The companion library `desync` provides the `pipe()` and `pipe_in()` functions +//! which can be used to schedule events generated by a stream. +//! +//! ## Example +//! +//! ``` +//! # use flo_binding::*; +//! let mut number = bind(1); +//! let number_clone = number.clone(); +//! let plusone = computed(move || number_clone.get() + 1); +//! +//! let mut lifetime = plusone.when_changed(notify(|| println!("Changed!"))); +//! +//! println!("{}", plusone.get()); // 2 +//! # assert!(plusone.get() == 2); +//! number.set(2); // 'Changed!' +//! println!("{}", plusone.get()); // 3 +//! # assert!(plusone.get() == 3); +//! lifetime.done(); +//! +//! number.set(3); // Lifetime is done, so no notification +//! println!("{}", plusone.get()); // 4 +//! # assert!(plusone.get() == 4); +//! ``` +//! +#![warn(bare_trait_objects)] + +mod bind_stream; +mod binding; +pub mod binding_context; +mod bindref; +mod computed; +mod follow; +mod notify_fn; +mod releasable; +mod rope_binding; +mod traits; + +pub use self::bind_stream::*; +pub use self::binding::*; +pub use self::bindref::*; +pub use self::computed::*; +pub use self::follow::*; +pub use self::notify_fn::*; +pub use self::rope_binding::*; +pub use self::traits::*; + +/// +/// Creates a simple bound value with the specified initial value +/// +pub fn bind<Value: Clone + PartialEq>(val: Value) -> Binding<Value> { + Binding::new(val) +} + +/// +/// Creates a computed value that tracks bindings accessed during the function call and marks itself as changed when any of these dependencies also change +/// +pub fn computed<Value, TFn>(calculate_value: TFn) -> ComputedBinding<Value, TFn> +where + Value: Clone + Send, + TFn: 'static + Send + Sync + Fn() -> Value, +{ + ComputedBinding::new(calculate_value) +} + +#[cfg(test)] +mod test { + use super::binding_context::*; + use super::*; + + use std::sync::*; + use std::thread; + use std::time::Duration; + + #[test] + fn can_create_binding() { + let bound = bind(1); + assert!(bound.get() == 1); + } + + #[test] + fn can_update_binding() { + let bound = bind(1); + + bound.set(2); + assert!(bound.get() == 2); + } + + #[test] + fn notified_on_change() { + let bound = bind(1); + let changed = bind(false); + + let notify_changed = changed.clone(); + bound + .when_changed(notify(move || notify_changed.set(true))) + .keep_alive(); + + assert!(changed.get() == false); + bound.set(2); + assert!(changed.get() == true); + } + + #[test] + fn not_notified_on_no_change() { + let bound = bind(1); + let changed = bind(false); + + let notify_changed = changed.clone(); + bound + .when_changed(notify(move || notify_changed.set(true))) + .keep_alive(); + + assert!(changed.get() == false); + bound.set(1); + assert!(changed.get() == false); + } + + #[test] + fn notifies_after_each_change() { + let bound = bind(1); + let change_count = bind(0); + + let notify_count = change_count.clone(); + bound + .when_changed(notify(move || { + let count = notify_count.get(); + notify_count.set(count + 1) + })) + .keep_alive(); + + assert!(change_count.get() == 0); + bound.set(2); + assert!(change_count.get() == 1); + + bound.set(3); + assert!(change_count.get() == 2); + + bound.set(4); + assert!(change_count.get() == 3); + } + + #[test] + fn dispatches_multiple_notifications() { + let bound = bind(1); + let change_count = bind(0); + + let notify_count = change_count.clone(); + let notify_count2 = change_count.clone(); + bound + .when_changed(notify(move || { + let count = notify_count.get(); + notify_count.set(count + 1) + })) + .keep_alive(); + bound + .when_changed(notify(move || { + let count = notify_count2.get(); + notify_count2.set(count + 1) + })) + .keep_alive(); + + assert!(change_count.get() == 0); + bound.set(2); + assert!(change_count.get() == 2); + + bound.set(3); + assert!(change_count.get() == 4); + + bound.set(4); + assert!(change_count.get() == 6); + } + + #[test] + fn stops_notifying_after_release() { + let bound = bind(1); + let change_count = bind(0); + + let notify_count = change_count.clone(); + let mut lifetime = bound.when_changed(notify(move || { + let count = notify_count.get(); + notify_count.set(count + 1) + })); + + assert!(change_count.get() == 0); + bound.set(2); + assert!(change_count.get() == 1); + + lifetime.done(); + assert!(change_count.get() == 1); + bound.set(3); + assert!(change_count.get() == 1); + } + + #[test] + fn release_only_affects_one_notification() { + let bound = bind(1); + let change_count = bind(0); + + let notify_count = change_count.clone(); + let notify_count2 = change_count.clone(); + let mut lifetime = bound.when_changed(notify(move || { + let count = notify_count.get(); + notify_count.set(count + 1) + })); + bound + .when_changed(notify(move || { + let count = notify_count2.get(); + notify_count2.set(count + 1) + })) + .keep_alive(); + + assert!(change_count.get() == 0); + bound.set(2); + assert!(change_count.get() == 2); + + bound.set(3); + assert!(change_count.get() == 4); + + bound.set(4); + assert!(change_count.get() == 6); + + lifetime.done(); + + bound.set(5); + assert!(change_count.get() == 7); + + bound.set(6); + assert!(change_count.get() == 8); + + bound.set(7); + assert!(change_count.get() == 9); + } + + #[test] + fn binding_context_is_notified() { + let bound = bind(1); + + bound.set(2); + + let (value, context) = BindingContext::bind(|| bound.get()); + assert!(value == 2); + + let changed = bind(false); + let notify_changed = changed.clone(); + context + .when_changed(notify(move || notify_changed.set(true))) + .keep_alive(); + + assert!(changed.get() == false); + bound.set(3); + assert!(changed.get() == true); + } + + #[test] + fn can_compute_value() { + let bound = bind(1); + + let computed_from = bound.clone(); + let computed = computed(move || computed_from.get() + 1); + + assert!(computed.get() == 2); + } + + #[test] + fn can_recompute_value() { + let bound = bind(1); + + let computed_from = bound.clone(); + let computed = computed(move || computed_from.get() + 1); + + assert!(computed.get() == 2); + + bound.set(2); + assert!(computed.get() == 3); + + bound.set(3); + assert!(computed.get() == 4); + } + + #[test] + fn can_recursively_compute_values() { + let bound = bind(1); + + let computed_from = bound.clone(); + let computed_val = computed(move || computed_from.get() + 1); + + let more_computed_from = computed_val.clone(); + let more_computed = computed(move || more_computed_from.get() + 1); + + assert!(computed_val.get() == 2); + assert!(more_computed.get() == 3); + + bound.set(2); + assert!(computed_val.get() == 3); + assert!(more_computed.get() == 4); + + bound.set(3); + assert!(computed_val.get() == 4); + assert!(more_computed.get() == 5); + } + + #[test] + fn can_recursively_compute_values_2() { + let bound = bind(1); + + let computed_from = bound.clone(); + let computed_val = computed(move || computed_from.get() + 1); + let more_computed = computed(move || computed_val.get() + 1); + + assert!(more_computed.get() == 3); + + bound.set(2); + assert!(more_computed.get() == 4); + + bound.set(3); + assert!(more_computed.get() == 5); + } + + #[test] + fn can_recursively_compute_values_3() { + let bound = bind(1); + + let computed_from = bound.clone(); + let computed_val = computed(move || computed_from.get() + 1); + let more_computed = computed(move || computed_val.get() + 1); + let even_more_computed = computed(move || more_computed.get() + 1); + + assert!(even_more_computed.get() == 4); + + bound.set(2); + assert!(even_more_computed.get() == 5); + + bound.set(3); + assert!(even_more_computed.get() == 6); + } + + #[test] + #[should_panic] + fn panics_if_computed_generated_during_binding() { + let bound = bind(1); + + let computed_from = bound.clone(); + let computed_val = computed(move || computed_from.get() + 1); + let even_more_computed = computed(move || { + let computed_val = computed_val.clone(); + + // This computed binding would be dropped after the first evaluation, which would result in the binding never updating. + // We should panic here. + let more_computed = computed(move || computed_val.get() + 1); + more_computed.get() + 1 + }); + + assert!(even_more_computed.get() == 4); + + bound.set(2); + assert!(even_more_computed.get() == 5); + + bound.set(3); + assert!(even_more_computed.get() == 6); + } + + #[test] + fn computed_only_recomputes_as_needed() { + let bound = bind(1); + + let counter = Arc::new(Mutex::new(0)); + let compute_counter = counter.clone(); + let computed_from = bound.clone(); + let computed = computed(move || { + let mut counter = compute_counter.lock().unwrap(); + *counter = *counter + 1; + + computed_from.get() + 1 + }); + + assert!(computed.get() == 2); + { + let counter = counter.lock().unwrap(); + assert!(counter.clone() == 1); + } + + assert!(computed.get() == 2); + { + let counter = counter.lock().unwrap(); + assert!(counter.clone() == 1); + } + + bound.set(2); + assert!(computed.get() == 3); + { + let counter = counter.lock().unwrap(); + assert!(counter.clone() == 2); + } + } + + #[test] + fn computed_caches_values() { + let update_count = Arc::new(Mutex::new(0)); + let bound = bind(1); + + let computed_update_count = Arc::clone(&update_count); + let computed_from = bound.clone(); + let computed = computed(move || { + let mut computed_update_count = computed_update_count.lock().unwrap(); + *computed_update_count += 1; + + computed_from.get() + 1 + }); + + assert!(computed.get() == 2); + assert!(*update_count.lock().unwrap() == 1); + + assert!(computed.get() == 2); + assert!(*update_count.lock().unwrap() == 1); + + bound.set(2); + assert!(computed.get() == 3); + assert!(*update_count.lock().unwrap() == 2); + + bound.set(3); + assert!(*update_count.lock().unwrap() == 2); + assert!(computed.get() == 4); + assert!(*update_count.lock().unwrap() == 3); + } + + #[test] + fn computed_notifies_of_changes() { + let bound = bind(1); + + let computed_from = bound.clone(); + let computed = computed(move || computed_from.get() + 1); + + let changed = bind(false); + let notify_changed = changed.clone(); + computed + .when_changed(notify(move || notify_changed.set(true))) + .keep_alive(); + + assert!(computed.get() == 2); + assert!(changed.get() == false); + + bound.set(2); + assert!(changed.get() == true); + assert!(computed.get() == 3); + + changed.set(false); + bound.set(3); + assert!(changed.get() == true); + assert!(computed.get() == 4); + } + + #[test] + fn computed_switches_dependencies() { + let switch = bind(false); + let val1 = bind(1); + let val2 = bind(2); + + let computed_switch = switch.clone(); + let computed_val1 = val1.clone(); + let computed_val2 = val2.clone(); + let computed = computed(move || { + // Use val1 when switch is false, and val2 when switch is true + if computed_switch.get() { + computed_val2.get() + 1 + } else { + computed_val1.get() + 1 + } + }); + + let changed = bind(false); + let notify_changed = changed.clone(); + computed + .when_changed(notify(move || notify_changed.set(true))) + .keep_alive(); + + // Initial value of computed (first get 'arms' when_changed too) + assert!(computed.get() == 2); + assert!(changed.get() == false); + + // Setting val2 shouldn't cause computed to become 'changed' initially + val2.set(3); + assert!(changed.get() == false); + assert!(computed.get() == 2); + + // ... but setting val1 should + val1.set(2); + assert!(changed.get() == true); + assert!(computed.get() == 3); + + // Flicking the switch will use the val2 value we set earlier + changed.set(false); + switch.set(true); + assert!(changed.get() == true); + assert!(computed.get() == 4); + + // Updating val2 should now mark us as changed + changed.set(false); + val2.set(4); + assert!(changed.get() == true); + assert!(computed.get() == 5); + + // Updating val1 should not mark us as changed + changed.set(false); + val1.set(5); + assert!(changed.get() == false); + assert!(computed.get() == 5); + } + + #[test] + fn change_during_computation_recomputes() { + // Create a computed binding that delays for a bit while reading + let some_binding = bind(1); + let some_computed = { + let some_binding = some_binding.clone(); + computed(move || { + let result = some_binding.get() + 1; + thread::sleep(Duration::from_millis(250)); + result + }) + }; + + // Start a thread that reads a value + { + let some_computed = some_computed.clone(); + thread::spawn(move || { + assert!(some_computed.get() == 2); + }); + } + + // Let the thread start running (give it enough time to start computing and reach the sleep statement) + // TODO: thread::sleep might fail on systems that are slow enough or due to glitches (will fail spuriously if we update the binding before the calculation starts) + thread::sleep(Duration::from_millis(10)); + + // Update the value in the binding while the computed is running + some_binding.set(2); + + // Computed value should update + assert!(some_computed.get() == 3); + } + + #[test] + fn computed_propagates_changes() { + let bound = bind(1); + + let computed_from = bound.clone(); + let propagates_from = computed(move || computed_from.get() + 1); + let computed_propagated = propagates_from.clone(); + let computed = computed(move || computed_propagated.get() + 1); + + let changed = bind(false); + let notify_changed = changed.clone(); + computed + .when_changed(notify(move || notify_changed.set(true))) + .keep_alive(); + + assert!(propagates_from.get() == 2); + assert!(computed.get() == 3); + assert!(changed.get() == false); + + bound.set(2); + assert!(propagates_from.get() == 3); + assert!(computed.get() == 4); + assert!(changed.get() == true); + + changed.set(false); + bound.set(3); + assert!(changed.get() == true); + assert!(propagates_from.get() == 4); + assert!(computed.get() == 5); + } + + #[test] + fn computed_stops_notifying_when_released() { + let bound = bind(1); + + let computed_from = bound.clone(); + let computed = computed(move || computed_from.get() + 1); + + let changed = bind(false); + let notify_changed = changed.clone(); + let mut lifetime = computed.when_changed(notify(move || notify_changed.set(true))); + + assert!(computed.get() == 2); + assert!(changed.get() == false); + + bound.set(2); + assert!(changed.get() == true); + assert!(computed.get() == 3); + + changed.set(false); + lifetime.done(); + + bound.set(3); + assert!(changed.get() == false); + assert!(computed.get() == 4); + + bound.set(4); + assert!(changed.get() == false); + assert!(computed.get() == 5); + } + + #[test] + fn computed_doesnt_notify_more_than_once() { + let bound = bind(1); + + let computed_from = bound.clone(); + let computed = computed(move || computed_from.get() + 1); + + let changed = bind(false); + let notify_changed = changed.clone(); + computed + .when_changed(notify(move || notify_changed.set(true))) + .keep_alive(); + + assert!(computed.get() == 2); + assert!(changed.get() == false); + + // Setting the value marks the computed as changed + bound.set(2); + assert!(changed.get() == true); + changed.set(false); + + // ... but when it's already changed we don't notify again + bound.set(3); + assert!(changed.get() == false); + + assert!(computed.get() == 4); + + // Once we've retrieved the value, we'll get notified of changes again + bound.set(4); + assert!(changed.get() == true); + } + + #[test] + fn computed_stops_notifying_once_out_of_scope() { + let bound = bind(1); + let changed = bind(false); + + { + let computed_from = bound.clone(); + let computed = computed(move || computed_from.get() + 1); + + let notify_changed = changed.clone(); + computed + .when_changed(notify(move || notify_changed.set(true))) + .keep_alive(); + + assert!(computed.get() == 2); + assert!(changed.get() == false); + + bound.set(2); + assert!(changed.get() == true); + assert!(computed.get() == 3); + }; + + // The computed value should have been disposed of so we should get no more notifications once we reach here + changed.set(false); + bound.set(3); + assert!(changed.get() == false); + } +} diff --git a/kayak_core/src/flo_binding/notify_fn.rs b/kayak_core/src/flo_binding/notify_fn.rs new file mode 100644 index 0000000..7c0cb20 --- /dev/null +++ b/kayak_core/src/flo_binding/notify_fn.rs @@ -0,0 +1,24 @@ +use super::traits::*; + +use std::sync::*; + +struct NotifyFn<TFn> { + when_changed: Mutex<TFn> +} + +impl<TFn> Notifiable for NotifyFn<TFn> +where TFn: Send+FnMut() -> () { + fn mark_as_changed(&self) { + let on_changed = &mut *self.when_changed.lock().unwrap(); + + on_changed() + } +} + +/// +/// Creates a notifiable reference from a function +/// +pub fn notify<TFn>(when_changed: TFn) -> Arc<dyn Notifiable> +where TFn: 'static+Send+FnMut() -> () { + Arc::new(NotifyFn { when_changed: Mutex::new(when_changed) }) +} diff --git a/kayak_core/src/flo_binding/releasable.rs b/kayak_core/src/flo_binding/releasable.rs new file mode 100644 index 0000000..72e2480 --- /dev/null +++ b/kayak_core/src/flo_binding/releasable.rs @@ -0,0 +1,135 @@ +use super::traits::*; + +use std::sync::*; + +// TODO: issue with new 'drop' behaviour is what to do when we clone, as if keep_alive is +// false on the clone then dropping the clone will also drop this object. Sometimes we +// want this behaviour and sometimes we don't. + +/// +/// A notifiable that can be released (and then tidied up later) +/// +pub struct ReleasableNotifiable { + /// Set to true if this object should not release on drop. Note this is not shared, + /// so the first ReleasableNotifiable in a group to be dropped where keep_alive + /// is false will mark all the others as done. + keep_alive: bool, + + /// The notifiable object that should be released when it's done + target: Arc<Mutex<Option<Arc<dyn Notifiable>>>> +} + +impl ReleasableNotifiable { + /// + /// Creates a new releasable notifiable object + /// + pub fn new(target: Arc<dyn Notifiable>) -> ReleasableNotifiable { + ReleasableNotifiable { + keep_alive: false, + target: Arc::new(Mutex::new(Some(target))) + } + } + + /// + /// Marks this as changed and returns whether or not the notification was called + /// + pub fn mark_as_changed(&self) -> bool { + // Get a reference to the target via the lock + let target = { + // Reset the optional item so that it's 'None' + let target = self.target.lock().unwrap(); + + // Send to the target + target.clone() + }; + + // Send to the target + if let Some(ref target) = target { + target.mark_as_changed(); + true + } else { + false + } + } + + /// + /// True if this item is still in use + /// + pub fn is_in_use(&self) -> bool { + self.target.lock().unwrap().is_some() + } + + /// + /// Creates a new 'owned' clone (which will expire this notifiable when dropped) + /// + pub fn clone_as_owned(&self) -> ReleasableNotifiable { + ReleasableNotifiable { + keep_alive: self.keep_alive, + target: Arc::clone(&self.target) + } + } + + /// + /// Creates a new 'inspection' clone (which can be dropped without ending + /// the lifetime of the releasable object) + /// + pub fn clone_for_inspection(&self) -> ReleasableNotifiable { + ReleasableNotifiable { + keep_alive: true, + target: Arc::clone(&self.target) + } + } +} + +impl Releasable for ReleasableNotifiable { + fn done(&mut self) { + // Reset the optional item so that it's 'None' + let mut target = self.target.lock().unwrap(); + + *target = None; + } + + fn keep_alive(&mut self) { + self.keep_alive = true; + } +} + +impl Notifiable for ReleasableNotifiable { + fn mark_as_changed(&self) { + // Get a reference to the target via the lock + let target = { + // Reset the optional item so that it's 'None' + let target = self.target.lock().unwrap(); + + // Send to the target + target.clone() + }; + + // Make sure we're calling out to mark_as_changed outside of the lock + if let Some(target) = target { + target.mark_as_changed(); + } + } +} + +impl Drop for ReleasableNotifiable { + fn drop(&mut self) { + if !self.keep_alive { + self.done(); + } + } +} + +impl Releasable for Vec<Box<dyn Releasable>> { + fn done(&mut self) { + for item in self.iter_mut() { + item.done(); + } + } + + fn keep_alive(&mut self) { + for item in self.iter_mut() { + item.keep_alive(); + } + } +} diff --git a/kayak_core/src/flo_binding/rope_binding/core.rs b/kayak_core/src/flo_binding/rope_binding/core.rs new file mode 100644 index 0000000..c807027 --- /dev/null +++ b/kayak_core/src/flo_binding/rope_binding/core.rs @@ -0,0 +1,125 @@ +use crate::flo_binding::releasable::*; +use crate::flo_binding::rope_binding::stream_state::*; + +use flo_rope::*; +use futures::task::*; + +/// +/// The core of a rope binding represents the data that's shared amongst all ropes +/// +pub(super) struct RopeBindingCore<Cell, Attribute> +where + Cell: Clone + PartialEq, + Attribute: Clone + PartialEq + Default, +{ + /// The number of items that are using hte core + pub(super) usage_count: usize, + + /// The rope that stores this binding + pub(super) rope: PullRope<AttributedRope<Cell, Attribute>, Box<dyn Fn() -> () + Send + Sync>>, + + /// The states of any streams reading from this rope + pub(super) stream_states: Vec<RopeStreamState<Cell, Attribute>>, + + #[allow(dead_code)] + /// The next ID to assign to a stream state + pub(super) next_stream_id: usize, + + // List of things to call when this binding changes + pub(super) when_changed: Vec<ReleasableNotifiable>, +} + +impl<Cell, Attribute> RopeBindingCore<Cell, Attribute> +where + Cell: 'static + Send + Unpin + Clone + PartialEq, + Attribute: 'static + Send + Sync + Clone + Unpin + PartialEq + Default, +{ + /// + /// If there are any notifiables in this object that aren't in use, remove them + /// + pub(super) fn filter_unused_notifications(&mut self) { + self.when_changed + .retain(|releasable| releasable.is_in_use()); + } + + /// + /// Callback: the rope has changes to pull + /// + pub(super) fn on_pull(&mut self) { + // Clear out any notifications that are not being used any more + self.filter_unused_notifications(); + + // Notify anything that's listening + for notifiable in &self.when_changed { + notifiable.mark_as_changed(); + } + + // Wake any streams that are waiting for changes to be pulled + for stream in self.stream_states.iter_mut() { + let waker = stream.waker.take(); + + if let Some(waker) = waker { + // Wake the stream so that it pulls the changes + waker.wake(); + } else { + // If the stream is trying to sleep, make sure it wakes up immediately + stream.needs_pull = true; + } + } + } + + /// + /// Pulls values from the rope and send to all attached streams + /// + pub(super) fn pull_rope(&mut self) { + // Stop the streams from waking up (no changes pending) + for stream in self.stream_states.iter_mut() { + stream.needs_pull = false; + } + + // Collect the actions + let actions = self.rope.pull_changes().collect::<Vec<_>>(); + + // Don't wake anything if there are no actions to perform + if actions.len() == 0 { + return; + } + + // Push to each stream + for stream in self.stream_states.iter_mut() { + stream.pending_changes.extend(actions.iter().cloned()); + } + + // Wake all of the streams + for stream in self.stream_states.iter_mut() { + let waker = stream.waker.take(); + waker.map(|waker| waker.wake()); + } + } + + /// + /// Wakes a particular stream when the rope changes + /// + pub(super) fn wake_stream(&mut self, stream_id: usize, waker: Waker) { + self.stream_states + .iter_mut() + .filter(|state| state.identifier == stream_id) + .nth(0) + .map(move |state| { + if !state.needs_pull { + // There are no pending values so we should wait for the rope to pull some extra data + + // Wake the stream when there's some more data to receive + state.waker = Some(waker); + } else { + // There are pending values so we should immediately re-awaken the stream + + // Disable the waker in case there's a stale one + state.waker = None; + + // Wake the stream so it reads the next value + waker.wake(); + } + }); + } +} diff --git a/kayak_core/src/flo_binding/rope_binding/mod.rs b/kayak_core/src/flo_binding/rope_binding/mod.rs new file mode 100644 index 0000000..a88c0d3 --- /dev/null +++ b/kayak_core/src/flo_binding/rope_binding/mod.rs @@ -0,0 +1,11 @@ +mod core; +mod rope_binding; +mod rope_binding_mut; +mod stream; +mod stream_state; +#[cfg(test)] +mod tests; + +pub use self::rope_binding::*; +pub use self::rope_binding_mut::*; +pub use self::stream::*; diff --git a/kayak_core/src/flo_binding/rope_binding/rope_binding.rs b/kayak_core/src/flo_binding/rope_binding/rope_binding.rs new file mode 100644 index 0000000..deca35b --- /dev/null +++ b/kayak_core/src/flo_binding/rope_binding/rope_binding.rs @@ -0,0 +1,255 @@ +use crate::flo_binding::releasable::*; +use crate::flo_binding::rope_binding::core::*; +use crate::flo_binding::rope_binding::stream::*; +use crate::flo_binding::rope_binding::stream_state::*; +use crate::flo_binding::traits::*; + +use ::desync::*; +use flo_rope::*; +use futures::prelude::*; + +use std::collections::VecDeque; +use std::ops::Range; +use std::sync::*; + +/// +/// A rope binding binds a vector of cells and attributes +/// +/// It's also possible to use a normal `Binding<Vec<_>>` for this purpose. A rope binding has a +/// couple of advantages though: it can handle very large collections of items and it can notify +/// only the relevant changes instead of always notifying the entire structure. +/// +/// Rope bindings are ideal for representing text areas in user interfaces, but can be used for +/// any collection data structure. +/// +pub struct RopeBinding<Cell, Attribute> +where + Cell: 'static + Send + Unpin + Clone + PartialEq, + Attribute: 'static + Send + Sync + Unpin + Clone + PartialEq + Default, +{ + /// The core of this binding + core: Arc<Desync<RopeBindingCore<Cell, Attribute>>>, +} + +impl<Cell, Attribute> RopeBinding<Cell, Attribute> +where + Cell: 'static + Send + Unpin + Clone + PartialEq, + Attribute: 'static + Send + Sync + Clone + Unpin + PartialEq + Default, +{ + /// + /// Creates a new rope binding from a stream of changes + /// + #[allow(dead_code)] + pub fn from_stream<S: 'static + Stream<Item = RopeAction<Cell, Attribute>> + Unpin + Send>( + stream: S, + ) -> RopeBinding<Cell, Attribute> { + // Create the core + let core = RopeBindingCore { + usage_count: 1, + rope: PullRope::from(AttributedRope::new(), Box::new(|| {})), + stream_states: vec![], + next_stream_id: 0, + when_changed: vec![], + }; + + let core = Arc::new(Desync::new(core)); + + // Recreate the rope in the core with a version that responds to pull events + let weak_core = Arc::downgrade(&core); + core.sync(move |core| { + core.rope = PullRope::from( + AttributedRope::new(), + Box::new(move || { + // Pass the event through to the core + let core = weak_core.upgrade(); + if let Some(core) = core { + core.desync(|core| core.on_pull()); + } + }), + ); + }); + + // Push changes through to the core rope from the stream + pipe_in(Arc::clone(&core), stream, |core, actions| { + async move { + core.rope.edit(actions); + } + .boxed() + }); + + // Create the binding + RopeBinding { core } + } + + /// + /// Creates a stream that follows the changes to this rope + /// + #[allow(dead_code)] + pub fn follow_changes(&self) -> RopeStream<Cell, Attribute> { + // Fetch an ID for the next stream from the core and generate a state + let stream_id = self.core.sync(|core| { + // Assign an ID to the stream + let next_id = core.next_stream_id; + core.next_stream_id += 1; + + // Create a state for this stream + let state = RopeStreamState { + identifier: next_id, + waker: None, + pending_changes: VecDeque::new(), + needs_pull: false, + }; + core.stream_states.push(state); + + // Return the stream ID + next_id + }); + + // Create the stream + RopeStream { + identifier: stream_id, + core: self.core.clone(), + poll_future: None, + draining: VecDeque::new(), + } + } + + /// + /// Returns the number of cells in this rope + /// + #[allow(dead_code)] + pub fn len(&self) -> usize { + self.core.sync(|core| core.rope.len()) + } + + /// + /// Reads the cell values for a range in this rope + /// + #[allow(dead_code)] + pub fn read_cells<'a>(&'a self, range: Range<usize>) -> impl 'a + Iterator<Item = Cell> { + // Read this range of cells by cloning from the core + let cells = self + .core + .sync(|core| core.rope.read_cells(range).cloned().collect::<Vec<_>>()); + + cells.into_iter() + } + + /// + /// Returns the attributes set at the specified location and their extent + /// + #[allow(dead_code)] + pub fn read_attributes<'a>(&'a self, pos: usize) -> (Attribute, Range<usize>) { + let (attribute, range) = self.core.sync(|core| { + let (attribute, range) = core.rope.read_attributes(pos); + (attribute.clone(), range) + }); + + (attribute, range) + } +} + +impl<Cell, Attribute> Clone for RopeBinding<Cell, Attribute> +where + Cell: 'static + Send + Unpin + Clone + PartialEq, + Attribute: 'static + Send + Sync + Clone + Unpin + PartialEq + Default, +{ + fn clone(&self) -> RopeBinding<Cell, Attribute> { + // Increase the usage count + let core = self.core.clone(); + core.desync(|core| core.usage_count += 1); + + // Create a new binding with the same core + RopeBinding { core } + } +} + +impl<Cell, Attribute> Drop for RopeBinding<Cell, Attribute> +where + Cell: 'static + Send + Unpin + Clone + PartialEq, + Attribute: 'static + Send + Sync + Clone + Unpin + PartialEq + Default, +{ + fn drop(&mut self) { + self.core.desync(|core| { + // Core is no longer in use + core.usage_count -= 1; + + // Counts as a notification if this is the last binding using this core + if core.usage_count == 0 { + core.pull_rope(); + } + }) + } +} + +impl<Cell, Attribute> Changeable for RopeBinding<Cell, Attribute> +where + Cell: 'static + Send + Unpin + Clone + PartialEq, + Attribute: 'static + Send + Sync + Clone + Unpin + PartialEq + Default, +{ + /// + /// Supplies a function to be notified when this item is changed + /// + /// This event is only fired after the value has been read since the most recent + /// change. Note that this means if the value is never read, this event may + /// never fire. This behaviour is desirable when deferring updates as it prevents + /// large cascades of 'changed' events occurring for complicated dependency trees. + /// + /// The releasable that's returned has keep_alive turned off by default, so + /// be sure to store it in a variable or call keep_alive() to keep it around + /// (if the event never seems to fire, this is likely to be the problem) + /// + fn when_changed(&self, what: Arc<dyn Notifiable>) -> Box<dyn Releasable> { + let releasable = ReleasableNotifiable::new(what); + let core_releasable = releasable.clone_as_owned(); + + self.core.desync(move |core| { + core.when_changed.push(core_releasable); + core.filter_unused_notifications(); + }); + + Box::new(releasable) + } +} + +/// +/// Trait implemented by something that is bound to a value +/// +impl<Cell, Attribute> Bound<AttributedRope<Cell, Attribute>> for RopeBinding<Cell, Attribute> +where + Cell: 'static + Send + Unpin + Clone + PartialEq, + Attribute: 'static + Send + Sync + Clone + Unpin + PartialEq + Default, +{ + /// + /// Retrieves the value stored by this binding + /// + fn get(&self) -> AttributedRope<Cell, Attribute> { + self.core.sync(|core| { + // Create a new rope from the existing one + let mut rope_copy = AttributedRope::new(); + + // Copy each attribute block one at a time + let len = core.rope.len(); + let mut pos = 0; + + while pos < len { + // Read the next range of attributes + let (attr, range) = core.rope.read_attributes(pos); + if range.len() == 0 { + pos += 1; + continue; + } + + // Write to the copy + let attr = attr.clone(); + let cells = core.rope.read_cells(range.clone()).cloned(); + rope_copy.replace_attributes(pos..pos, cells, attr); + + // Continue writing at the end of the new rope + pos = range.end; + } + + rope_copy + }) + } +} diff --git a/kayak_core/src/flo_binding/rope_binding/rope_binding_mut.rs b/kayak_core/src/flo_binding/rope_binding/rope_binding_mut.rs new file mode 100644 index 0000000..87faf09 --- /dev/null +++ b/kayak_core/src/flo_binding/rope_binding/rope_binding_mut.rs @@ -0,0 +1,287 @@ +use crate::flo_binding::releasable::*; +use crate::flo_binding::rope_binding::core::*; +use crate::flo_binding::rope_binding::stream::*; +use crate::flo_binding::rope_binding::stream_state::*; +use crate::flo_binding::traits::*; + +use ::desync::*; +use flo_rope::*; + +use std::collections::VecDeque; +use std::ops::Range; +use std::sync::*; + +/// +/// A rope binding binds a vector of cells and attributes +/// +/// A `RopeBindingMut` supplies the same functionality as a `RopeBinding` except it also provides the +/// editing functions for changing the underlying data. +/// +pub struct RopeBindingMut<Cell, Attribute> +where + Cell: 'static + Send + Unpin + Clone + PartialEq, + Attribute: 'static + Send + Sync + Unpin + Clone + PartialEq + Default, +{ + /// The core of this binding + core: Arc<Desync<RopeBindingCore<Cell, Attribute>>>, +} + +impl<Cell, Attribute> RopeBindingMut<Cell, Attribute> +where + Cell: 'static + Send + Unpin + Clone + PartialEq, + Attribute: 'static + Send + Sync + Clone + Unpin + PartialEq + Default, +{ + /// + /// Creates a new rope binding from a stream of changes + /// + #[allow(dead_code)] + pub fn new() -> RopeBindingMut<Cell, Attribute> { + // Create the core + let core = RopeBindingCore { + usage_count: 1, + rope: PullRope::from(AttributedRope::new(), Box::new(|| {})), + stream_states: vec![], + next_stream_id: 0, + when_changed: vec![], + }; + + let core = Arc::new(Desync::new(core)); + + // Recreate the rope in the core with a version that responds to pull events + let weak_core = Arc::downgrade(&core); + core.sync(move |core| { + core.rope = PullRope::from( + AttributedRope::new(), + Box::new(move || { + // Pass the event through to the core + let core = weak_core.upgrade(); + if let Some(core) = core { + core.desync(|core| core.on_pull()); + } + }), + ); + }); + + // Create the binding + RopeBindingMut { core } + } + + /// + /// Creates a stream that follows the changes to this rope + /// + #[allow(dead_code)] + pub fn follow_changes(&self) -> RopeStream<Cell, Attribute> { + // Fetch an ID for the next stream from the core and generate a state + let stream_id = self.core.sync(|core| { + // Assign an ID to the stream + let next_id = core.next_stream_id; + core.next_stream_id += 1; + + // Create a state for this stream + let state = RopeStreamState { + identifier: next_id, + waker: None, + pending_changes: VecDeque::new(), + needs_pull: false, + }; + core.stream_states.push(state); + + // Return the stream ID + next_id + }); + + // Create the stream + RopeStream { + identifier: stream_id, + core: self.core.clone(), + poll_future: None, + draining: VecDeque::new(), + } + } + + /// + /// Returns the number of cells in this rope + /// + #[allow(dead_code)] + pub fn len(&self) -> usize { + self.core.sync(|core| core.rope.len()) + } + + /// + /// Reads the cell values for a range in this rope + /// + #[allow(dead_code)] + pub fn read_cells<'a>(&'a self, range: Range<usize>) -> impl 'a + Iterator<Item = Cell> { + // Read this range of cells by cloning from the core + let cells = self + .core + .sync(|core| core.rope.read_cells(range).cloned().collect::<Vec<_>>()); + + cells.into_iter() + } + + /// + /// Returns the attributes set at the specified location and their extent + /// + #[allow(dead_code)] + pub fn read_attributes<'a>(&'a self, pos: usize) -> (Attribute, Range<usize>) { + let (attribute, range) = self.core.sync(|core| { + let (attribute, range) = core.rope.read_attributes(pos); + (attribute.clone(), range) + }); + + (attribute, range) + } + + /// + /// Performs the specified editing action to this rope + /// + #[allow(dead_code)] + pub fn edit(&mut self, action: RopeAction<Cell, Attribute>) { + self.core.sync(move |core| core.rope.edit(action)); + } + + /// + /// Replaces a range of cells. The attributes applied to the new cells will be the same + /// as the attributes that were applied to the first cell in the replacement range + /// + #[allow(dead_code)] + pub fn replace<NewCells: 'static + Send + IntoIterator<Item = Cell>>( + &mut self, + range: Range<usize>, + new_cells: NewCells, + ) { + self.core + .sync(move |core| core.rope.replace(range, new_cells)); + } + + /// + /// Sets the attributes for a range of cells + /// + #[allow(dead_code)] + pub fn set_attributes(&mut self, range: Range<usize>, new_attributes: Attribute) { + self.core + .sync(move |core| core.rope.set_attributes(range, new_attributes)); + } + + /// + /// Replaces a range of cells and sets the attributes for them. + /// + #[allow(dead_code)] + pub fn replace_attributes<NewCells: 'static + Send + IntoIterator<Item = Cell>>( + &mut self, + range: Range<usize>, + new_cells: NewCells, + new_attributes: Attribute, + ) { + self.core.sync(move |core| { + core.rope + .replace_attributes(range, new_cells, new_attributes) + }); + } +} + +impl<Cell, Attribute> Clone for RopeBindingMut<Cell, Attribute> +where + Cell: 'static + Send + Unpin + Clone + PartialEq, + Attribute: 'static + Send + Sync + Clone + Unpin + PartialEq + Default, +{ + fn clone(&self) -> RopeBindingMut<Cell, Attribute> { + // Increase the usage count + let core = self.core.clone(); + core.desync(|core| core.usage_count += 1); + + // Create a new binding with the same core + RopeBindingMut { core } + } +} + +impl<Cell, Attribute> Drop for RopeBindingMut<Cell, Attribute> +where + Cell: 'static + Send + Unpin + Clone + PartialEq, + Attribute: 'static + Send + Sync + Clone + Unpin + PartialEq + Default, +{ + fn drop(&mut self) { + self.core.desync(|core| { + // Core is no longer in use + core.usage_count -= 1; + + // Counts as a notification if this is the last binding using this core + if core.usage_count == 0 { + core.pull_rope(); + } + }) + } +} + +impl<Cell, Attribute> Changeable for RopeBindingMut<Cell, Attribute> +where + Cell: 'static + Send + Unpin + Clone + PartialEq, + Attribute: 'static + Send + Sync + Clone + Unpin + PartialEq + Default, +{ + /// + /// Supplies a function to be notified when this item is changed + /// + /// This event is only fired after the value has been read since the most recent + /// change. Note that this means if the value is never read, this event may + /// never fire. This behaviour is desirable when deferring updates as it prevents + /// large cascades of 'changed' events occurring for complicated dependency trees. + /// + /// The releasable that's returned has keep_alive turned off by default, so + /// be sure to store it in a variable or call keep_alive() to keep it around + /// (if the event never seems to fire, this is likely to be the problem) + /// + fn when_changed(&self, what: Arc<dyn Notifiable>) -> Box<dyn Releasable> { + let releasable = ReleasableNotifiable::new(what); + let core_releasable = releasable.clone_as_owned(); + + self.core.desync(move |core| { + core.when_changed.push(core_releasable); + core.filter_unused_notifications(); + }); + + Box::new(releasable) + } +} + +/// +/// Trait implemented by something that is bound to a value +/// +impl<Cell, Attribute> Bound<AttributedRope<Cell, Attribute>> for RopeBindingMut<Cell, Attribute> +where + Cell: 'static + Send + Unpin + Clone + PartialEq, + Attribute: 'static + Send + Sync + Clone + Unpin + PartialEq + Default, +{ + /// + /// Retrieves the value stored by this binding + /// + fn get(&self) -> AttributedRope<Cell, Attribute> { + self.core.sync(|core| { + // Create a new rope from the existing one + let mut rope_copy = AttributedRope::new(); + + // Copy each attribute block one at a time + let len = core.rope.len(); + let mut pos = 0; + + while pos < len { + // Read the next range of attributes + let (attr, range) = core.rope.read_attributes(pos); + if range.len() == 0 { + pos += 1; + continue; + } + + // Write to the copy + let attr = attr.clone(); + let cells = core.rope.read_cells(range.clone()).cloned(); + rope_copy.replace_attributes(pos..pos, cells, attr); + + // Continue writing at the end of the new rope + pos = range.end; + } + + rope_copy + }) + } +} diff --git a/kayak_core/src/flo_binding/rope_binding/stream.rs b/kayak_core/src/flo_binding/rope_binding/stream.rs new file mode 100644 index 0000000..87d70cf --- /dev/null +++ b/kayak_core/src/flo_binding/rope_binding/stream.rs @@ -0,0 +1,161 @@ +use crate::flo_binding::rope_binding::core::*; + +use ::desync::*; +use flo_rope::*; +use futures::future::BoxFuture; +use futures::prelude::*; +use futures::task::*; + +use std::collections::VecDeque; +use std::mem; +use std::pin::*; +use std::sync::*; + +/// +/// A rope stream monitors a rope binding, and supplies them as a stream so they can be mirrored elsewhere +/// +/// An example of a use for a rope stream is to send updates from a rope to a user interface. +/// +pub struct RopeStream<Cell, Attribute> +where + Cell: 'static + Send + Unpin + Clone + PartialEq, + Attribute: 'static + Send + Sync + Clone + Unpin + PartialEq + Default, +{ + /// The identifier for this stream + pub(super) identifier: usize, + + /// The core of the rope + pub(super) core: Arc<Desync<RopeBindingCore<Cell, Attribute>>>, + + /// A future that will return the next poll result + pub(super) poll_future: + Option<BoxFuture<'static, Poll<Option<VecDeque<RopeAction<Cell, Attribute>>>>>>, + + /// The actions that are currently being drained through this stream + pub(super) draining: VecDeque<RopeAction<Cell, Attribute>>, +} + +impl<Cell, Attribute> Stream for RopeStream<Cell, Attribute> +where + Cell: 'static + Send + Unpin + Clone + PartialEq, + Attribute: 'static + Send + Sync + Clone + Unpin + PartialEq + Default, +{ + type Item = RopeAction<Cell, Attribute>; + + fn poll_next( + mut self: Pin<&mut Self>, + ctxt: &mut Context<'_>, + ) -> Poll<Option<RopeAction<Cell, Attribute>>> { + // If we've got a set of actions we're already reading, then return those as fast as we can + if self.draining.len() > 0 { + return Poll::Ready(self.draining.pop_back()); + } + + // If we're waiting for the core to return to us, borrow the future from there + let poll_future = self.poll_future.take(); + let mut poll_future = if let Some(poll_future) = poll_future { + // We're already waiting for the core to get back to us + poll_future + } else { + // Ask the core for the next stream state + let stream_id = self.identifier; + + self.core + .future_desync(move |core| { + async move { + // Pull any pending changes from the rope + core.pull_rope(); + + // Find the state of this stream + let stream_state = core + .stream_states + .iter_mut() + .filter(|state| state.identifier == stream_id) + .nth(0) + .unwrap(); + + // Check for data + if stream_state.pending_changes.len() > 0 { + // Return the changes to the waiting stream + let mut changes = VecDeque::new(); + mem::swap(&mut changes, &mut stream_state.pending_changes); + + Poll::Ready(Some(changes)) + } else if core.usage_count == 0 { + // No changes, and nothing is using the core any more + Poll::Ready(None) + } else { + // No changes are waiting + Poll::Pending + } + } + .boxed() + }) + .map(|result| { + // Error would indicate the core had gone away before the request should complete, so we signal this as an end-of-stream event + match result { + Ok(result) => result, + Err(_) => Poll::Ready(None), + } + }) + .boxed() + }; + + // Ask the future for the latest update on this stream + let future_result = poll_future.poll_unpin(ctxt); + + match future_result { + Poll::Ready(Poll::Ready(Some(actions))) => { + if actions.len() == 0 { + // Nothing waiting: need to wait until the rope signals a 'pull' event + let waker = ctxt.waker().clone(); + let stream_id = self.identifier; + + self.core.desync(move |core| { + core.wake_stream(stream_id, waker); + }); + + Poll::Pending + } else { + // Have some actions ready + self.draining = actions; + Poll::Ready(self.draining.pop_back()) + } + } + + Poll::Ready(Poll::Ready(None)) => Poll::Ready(None), + Poll::Ready(Poll::Pending) => { + // Wake when the rope generates a 'pull' event + let waker = ctxt.waker().clone(); + let stream_id = self.identifier; + + self.core.desync(move |core| { + core.wake_stream(stream_id, waker); + }); + + Poll::Pending + } + + Poll::Pending => { + // Poll the future again when it notifies + self.poll_future = Some(poll_future); + Poll::Pending + } + } + } +} + +impl<Cell, Attribute> Drop for RopeStream<Cell, Attribute> +where + Cell: 'static + Send + Unpin + Clone + PartialEq, + Attribute: 'static + Send + Sync + Clone + Unpin + PartialEq + Default, +{ + fn drop(&mut self) { + // Remove the stream state when the stream is no more + let dropped_stream_id = self.identifier; + self.core.desync(move |core| { + core.stream_states + .retain(|state| state.identifier != dropped_stream_id); + }); + } +} diff --git a/kayak_core/src/flo_binding/rope_binding/stream_state.rs b/kayak_core/src/flo_binding/rope_binding/stream_state.rs new file mode 100644 index 0000000..5d1bf10 --- /dev/null +++ b/kayak_core/src/flo_binding/rope_binding/stream_state.rs @@ -0,0 +1,24 @@ +use flo_rope::*; +use futures::task::*; + +use std::collections::{VecDeque}; + +/// +/// The state of a stream that is reading from a rope binding core +/// +pub (super) struct RopeStreamState<Cell, Attribute> +where +Cell: Clone+PartialEq, +Attribute: Clone+PartialEq+Default { + /// The identifier for this stream + pub (super) identifier: usize, + + /// The waker for the current stream + pub (super) waker: Option<Waker>, + + /// The changes that are waiting to be sent to this stream + pub (super) pending_changes: VecDeque<RopeAction<Cell, Attribute>>, + + /// True if the rope has indicated there are changes waiting to be pulled + pub (super) needs_pull: bool +} diff --git a/kayak_core/src/flo_binding/rope_binding/tests.rs b/kayak_core/src/flo_binding/rope_binding/tests.rs new file mode 100644 index 0000000..e28a6e1 --- /dev/null +++ b/kayak_core/src/flo_binding/rope_binding/tests.rs @@ -0,0 +1,44 @@ +use crate::flo_binding::*; + +use flo_rope::*; + +use futures::executor; +use futures::prelude::*; + +#[test] +fn mutable_rope_sends_changes_to_stream() { + // Create a rope that copies changes from a mutable rope + let mut mutable_rope = RopeBindingMut::<usize, ()>::new(); + let mut rope_stream = mutable_rope.follow_changes(); + + // Write some data to the mutable rope + mutable_rope.replace(0..0, vec![1, 2, 3, 4]); + + // Should get sent to the stream + executor::block_on(async move { + let next = rope_stream.next().await; + + assert!(next == Some(RopeAction::Replace(0..0, vec![1, 2, 3, 4]))); + }); +} + +#[test] +fn pull_from_mutable_binding() { + // Create a rope that copies changes from a mutable rope + let mut mutable_rope = RopeBindingMut::<usize, ()>::new(); + let rope_copy = RopeBinding::from_stream(mutable_rope.follow_changes()); + let mut rope_stream = rope_copy.follow_changes(); + + // Write some data to the mutable rope + mutable_rope.replace(0..0, vec![1, 2, 3, 4]); + + // Wait for the change to arrive at the copy + executor::block_on(async move { + let next = rope_stream.next().await; + assert!(next == Some(RopeAction::Replace(0..0, vec![1, 2, 3, 4]))) + }); + + // Read from the copy + assert!(rope_copy.len() == 4); + assert!(rope_copy.read_cells(0..4).collect::<Vec<_>>() == vec![1, 2, 3, 4]); +} diff --git a/kayak_core/src/flo_binding/traits.rs b/kayak_core/src/flo_binding/traits.rs new file mode 100644 index 0000000..a651ede --- /dev/null +++ b/kayak_core/src/flo_binding/traits.rs @@ -0,0 +1,89 @@ +use std::sync::*; + +/// +/// Trait implemented by items with dependencies that need to be notified when they have changed +/// +pub trait Notifiable : Sync+Send { + /// + /// Indicates that a dependency of this object has changed + /// + fn mark_as_changed(&self); +} + +/// +/// Trait implemented by an object that can be released: for example to stop performing +/// an action when it's no longer required. +/// +pub trait Releasable : Send+Sync { + /// + /// Indicates that this object should not be released on drop + /// + fn keep_alive(&mut self); + + /// + /// Indicates that this object is finished with and should be released + /// + fn done(&mut self); +} + +/// +/// Trait implemented by items that can notify something when they're changed +/// +pub trait Changeable { + /// + /// Supplies a function to be notified when this item is changed + /// + /// This event is only fired after the value has been read since the most recent + /// change. Note that this means if the value is never read, this event may + /// never fire. This behaviour is desirable when deferring updates as it prevents + /// large cascades of 'changed' events occurring for complicated dependency trees. + /// + /// The releasable that's returned has keep_alive turned off by default, so + /// be sure to store it in a variable or call keep_alive() to keep it around + /// (if the event never seems to fire, this is likely to be the problem) + /// + fn when_changed(&self, what: Arc<dyn Notifiable>) -> Box<dyn Releasable>; +} + +/// +/// Trait implemented by something that is bound to a value +/// +pub trait Bound<Value> : Changeable+Send+Sync { + /// + /// Retrieves the value stored by this binding + /// + fn get(&self) -> Value; +} + +/// +/// Trait implemented by something that is bound to a value +/// +// Seperate Trait to allow Bound to be made into an object for BindRef +pub trait WithBound<Value>: Changeable + Send + Sync { + /// + /// Mutate instead of replacing value stored in this binding, return true + /// to send notifiations + /// + fn with_ref<F, T>(&self, f: F) -> T + where + F: FnOnce(&Value) -> T; + /// + /// Mutate instead of replacing value stored in this binding, return true + /// to send notifiations + /// + fn with_mut<F>(&self, f: F) + where + F: FnOnce(&mut Value) -> bool; +} +/// +/// Trait implemented by something that is bound to a value that can be changed +/// +/// Bindings are similar in behaviour to Arc<Mutex<Value>>, so it's possible to set +/// the value of their target even when the binding itself is not mutable. +/// +pub trait MutableBound<Value> : Bound<Value> { + /// + /// Sets the value stored by this binding + /// + fn set(&self, new_value: Value); +} diff --git a/kayak_core/src/lib.rs b/kayak_core/src/lib.rs index 36edf12..106185e 100644 --- a/kayak_core/src/lib.rs +++ b/kayak_core/src/lib.rs @@ -19,6 +19,8 @@ pub mod widget_manager; mod cursor; mod event_dispatcher; mod keyboard; +mod assets; +mod flo_binding; use std::sync::{Arc, RwLock}; diff --git a/kayak_core/src/render_command.rs b/kayak_core/src/render_command.rs index c8ebc25..510d267 100644 --- a/kayak_core/src/render_command.rs +++ b/kayak_core/src/render_command.rs @@ -10,7 +10,8 @@ pub enum RenderCommand { Text { content: String, size: f32, - font: u16, + font: String, + parent_size: (f32, f32), }, Image { handle: u16, diff --git a/kayak_core/src/render_primitive.rs b/kayak_core/src/render_primitive.rs index 0fa313e..4fe7734 100644 --- a/kayak_core/src/render_primitive.rs +++ b/kayak_core/src/render_primitive.rs @@ -21,7 +21,8 @@ pub enum RenderPrimitive { color: Color, size: f32, content: String, - font: u16, + font: String, + parent_size: (f32, f32), }, Image { layout: Rect, @@ -72,12 +73,14 @@ impl From<&Style> for RenderPrimitive { content, size, font, + parent_size, } => Self::Text { layout: Rect::default(), color: style.color.resolve(), size, content, font, + parent_size, }, RenderCommand::Image { handle } => Self::Image { layout: Rect::default(), diff --git a/kayak_core/src/widget_manager.rs b/kayak_core/src/widget_manager.rs index 3e41213..12e373e 100644 --- a/kayak_core/src/widget_manager.rs +++ b/kayak_core/src/widget_manager.rs @@ -3,6 +3,7 @@ use std::{ sync::{Arc, Mutex}, }; +use crate::layout_cache::Rect; use crate::{ layout_cache::LayoutCache, node::{Node, NodeBuilder}, @@ -12,7 +13,6 @@ use crate::{ tree::Tree, Arena, Index, Widget, }; -use crate::layout_cache::Rect; // use as_any::Downcast; #[derive(Debug)] @@ -375,17 +375,14 @@ impl WidgetManager { children } - fn get_valid_parent(&self, node_id: Index) -> Option<Index> { + pub fn get_valid_parent(&self, node_id: Index) -> Option<Index> { if let Some(parent_id) = self.tree.parents.get(&node_id) { - if let Some(parent_widget) = &self.current_widgets[*parent_id] { - if let Some(parent_styles) = parent_widget.get_styles() { - if parent_styles.render_command.resolve() != RenderCommand::Empty { - return Some(*parent_id); - } + if let Some(parent_widget) = &self.nodes[*parent_id] { + if parent_widget.styles.render_command.resolve() != RenderCommand::Empty { + return Some(*parent_id); } - - return self.get_valid_parent(*parent_id); } + return self.get_valid_parent(*parent_id); } None } diff --git a/kayak_core/tests/mod.rs b/kayak_core/tests/mod.rs deleted file mode 100644 index 6b9848f..0000000 --- a/kayak_core/tests/mod.rs +++ /dev/null @@ -1,31 +0,0 @@ -#[test] -fn flo_binding_test() { - use flo_binding::{Binding, Bound, Changeable, MutableBound}; - - #[derive(Clone, PartialEq)] - struct TestState { - pub value: u32, - } - - let mut some_state = resources::Resources::default(); - - let test_state = flo_binding::bind(TestState { value: 0 }); - - let mut lifetime = test_state.when_changed(flo_binding::notify(|| { - dbg!("Changed!"); - })); - - some_state.insert(test_state); - - if let Ok(test_state) = some_state.get::<Binding<TestState>>() { - assert!(test_state.get().value == 0); - - test_state.set(TestState { value: 2 }); - - assert!(test_state.get().value == 2); - - lifetime.done(); - }; - - panic!(); -} diff --git a/kayak_font/src/atlas.rs b/kayak_font/src/atlas.rs index 58f6537..cb90b95 100644 --- a/kayak_font/src/atlas.rs +++ b/kayak_font/src/atlas.rs @@ -1,12 +1,12 @@ use serde::Deserialize; -#[derive(Deserialize, Debug, Copy, Clone)] +#[derive(Deserialize, Debug, Copy, Clone, PartialEq)] pub enum SDFType { #[serde(alias = "msdf")] Msdf, } -#[derive(Deserialize, Debug, Copy, Clone)] +#[derive(Deserialize, Debug, Copy, Clone, PartialEq)] pub enum Origin { #[serde(alias = "bottom")] Bottom, @@ -18,7 +18,7 @@ pub enum Origin { Top, } -#[derive(Deserialize, Debug, Copy, Clone)] +#[derive(Deserialize, Debug, Copy, Clone, PartialEq)] pub struct Atlas { #[serde(alias = "type")] pub sdf_type: SDFType, diff --git a/kayak_font/src/font.rs b/kayak_font/src/font.rs index 72307f3..10bba52 100644 --- a/kayak_font/src/font.rs +++ b/kayak_font/src/font.rs @@ -6,7 +6,7 @@ use bevy::{prelude::Handle, reflect::TypeUuid, render::texture::Image}; use crate::Sdf; #[cfg(feature = "bevy_renderer")] -#[derive(Debug, Clone, TypeUuid)] +#[derive(Debug, Clone, TypeUuid, PartialEq)] #[uuid = "4fe4732c-6731-49bb-bafc-4690d636b848"] pub struct KayakFont { pub sdf: Sdf, @@ -85,6 +85,61 @@ impl KayakFont { width } + pub fn measure( + &self, + axis_alignment: CoordinateSystem, + content: &String, + font_size: f32, + line_height: f32, + max_size: (f32, f32), + ) -> (f32, f32) { + let mut size: (f32, f32) = (0.0, 0.0); + let split_chars = vec![' ', '\t', '-', '\n']; + let missing_chars: Vec<char> = content + .chars() + .filter(|c| split_chars.iter().any(|c2| c == c2)) + .collect(); + + let shift_sign = match axis_alignment { + CoordinateSystem::PositiveYDown => -1.0, + CoordinateSystem::PositiveYUp => 1.0, + }; + + let mut x = 0.0; + let mut y = 0.0; + let mut i = 0; + for word in content.split(&split_chars[..]) { + let word_width = self.get_word_width(word, font_size); + if x + word_width > max_size.0 { + y -= shift_sign * line_height; + x = 0.0; + } + for c in word.chars() { + if let Some(glyph) = self.sdf.glyphs.iter().find(|glyph| glyph.unicode == c) { + x += glyph.advance * font_size; + size.0 = size.0.max(x); + } + } + + if let Some(next_missing) = missing_chars.get(i) { + if let Some(glyph) = self + .sdf + .glyphs + .iter() + .find(|glyph| glyph.unicode == *next_missing) + { + x += glyph.advance * font_size; + } + i += 1; + } + } + // One last shift.. + y -= shift_sign * line_height; + size.1 = y.abs(); + + size + } + pub fn get_layout( &self, axis_alignment: CoordinateSystem, @@ -121,7 +176,7 @@ impl KayakFont { let mut last_width = 0.0; for word in content.split(&split_chars[..]) { let word_width = self.get_word_width(word, font_size); - if x + word_width > max_size.0 { + if x + word_width + (font_size / 2.0) > max_size.0 { y -= shift_sign * line_height; line_widths.push((x, line_starting_index, positions_and_size.len())); line_starting_index = positions_and_size.len(); diff --git a/kayak_font/src/glyph.rs b/kayak_font/src/glyph.rs index c0b1638..54301e0 100644 --- a/kayak_font/src/glyph.rs +++ b/kayak_font/src/glyph.rs @@ -11,7 +11,7 @@ where } } -#[derive(Deserialize, Debug, Clone, Copy)] +#[derive(Deserialize, Debug, Clone, Copy, PartialEq)] pub struct Glyph { #[serde(deserialize_with = "from_u32")] pub unicode: char, @@ -22,7 +22,7 @@ pub struct Glyph { pub plane_bounds: Option<Rect>, } -#[derive(Deserialize, Default, Clone, Copy, Debug)] +#[derive(Deserialize, Default, Clone, Copy, Debug, PartialEq)] pub struct Rect { pub left: f32, pub bottom: f32, diff --git a/kayak_font/src/metrics.rs b/kayak_font/src/metrics.rs index 005e9bb..0e042bb 100644 --- a/kayak_font/src/metrics.rs +++ b/kayak_font/src/metrics.rs @@ -1,6 +1,6 @@ use serde::Deserialize; -#[derive(Deserialize, Debug, Copy, Clone)] +#[derive(Deserialize, Debug, Copy, Clone, PartialEq)] pub struct Metrics { #[serde(alias = "emSize")] em_size: f32, diff --git a/kayak_font/src/sdf.rs b/kayak_font/src/sdf.rs index 49e5453..13fe4d9 100644 --- a/kayak_font/src/sdf.rs +++ b/kayak_font/src/sdf.rs @@ -1,7 +1,7 @@ use crate::{atlas::Atlas, glyph::Glyph, metrics::Metrics}; use serde::Deserialize; -#[derive(Deserialize, Debug, Clone)] +#[derive(Deserialize, Debug, Clone, PartialEq)] pub struct Sdf { pub atlas: Atlas, metrics: Metrics, @@ -9,7 +9,7 @@ pub struct Sdf { kerning: Vec<KerningData>, } -#[derive(Deserialize, Debug, Clone, Copy)] +#[derive(Deserialize, Debug, Clone, Copy, PartialEq)] pub struct KerningData { pub unicode1: u32, pub unicode2: u32, diff --git a/src/widgets/app.rs b/src/widgets/app.rs index 2d61138..69c853c 100644 --- a/src/widgets/app.rs +++ b/src/widgets/app.rs @@ -10,11 +10,6 @@ use crate::widgets::Clip; #[widget] pub fn App(children: Children) { - *styles = Some(Style { - render_command: StyleProp::Value(RenderCommand::Layout), - ..styles.clone().unwrap_or_default() - }); - #[cfg(feature = "bevy_renderer")] { use crate::bevy::WindowSize; @@ -32,7 +27,9 @@ pub fn App(children: Children) { context.bind(&window_size); let window_size = window_size.get(); + dbg!(window_size); *styles = Some(Style { + render_command: StyleProp::Value(RenderCommand::Layout), width: StyleProp::Value(Units::Pixels(window_size.0)), height: StyleProp::Value(Units::Pixels(window_size.1)), ..styles.clone().unwrap_or_default() diff --git a/src/widgets/clip.rs b/src/widgets/clip.rs index 0a0fec4..d7d41c6 100644 --- a/src/widgets/clip.rs +++ b/src/widgets/clip.rs @@ -11,8 +11,8 @@ pub fn Clip(children: Children, styles: Option<Style>) { render_command: StyleProp::Value(RenderCommand::Clip), width: StyleProp::Value(Units::Stretch(1.0)), height: StyleProp::Value(Units::Stretch(1.0)), - min_width: StyleProp::Value(Units::Stretch(1.0)), - min_height: StyleProp::Value(Units::Stretch(1.0)), + // min_width: StyleProp::Value(Units::Stretch(1.0)), + // min_height: StyleProp::Value(Units::Stretch(1.0)), ..styles.clone().unwrap_or_default() }); rsx! { diff --git a/src/widgets/text.rs b/src/widgets/text.rs index 14979ee..e66237b 100644 --- a/src/widgets/text.rs +++ b/src/widgets/text.rs @@ -1,3 +1,6 @@ +use kayak_core::{styles::Units, Binding, Bound}; +use kayak_font::{CoordinateSystem, KayakFont}; + use crate::core::{ render_command::RenderCommand, styles::{Style, StyleProp}, @@ -5,14 +8,48 @@ use crate::core::{ }; #[widget] -pub fn Text(size: f32, content: String, styles: Option<Style>, font: Option<u16>) { +pub fn Text(size: f32, content: String, styles: Option<Style>, font: Option<String>) { + let font_name = font; + let font: Binding<Option<KayakFont>> = + context.get_asset(font_name.clone().unwrap_or("Roboto".into())); + + context.bind(&font); + + // TODO: It might be worth caching the measurement here until content changes. + let (layout_size, parent_size) = + if let Some(parent_id) = context.widget_manager.get_valid_parent(parent_id.unwrap()) { + if let Some(layout) = context.widget_manager.get_layout(&parent_id) { + if let Some(font) = font.get() { + let measurement = font.measure( + CoordinateSystem::PositiveYDown, + &content, + size, + size * 1.2, + (layout.width, layout.height), + ); + + (measurement, (layout.width, layout.height)) + } else { + ((0.0, 0.0), (layout.width, layout.height)) + } + } else { + ((0.0, 0.0), (0.0, 0.0)) + } + } else { + ((0.0, 0.0), (0.0, 0.0)) + }; + let render_command = RenderCommand::Text { - content, + content: content.clone(), size, - font: font.unwrap_or(0), + parent_size, + font: font_name.clone().unwrap_or("Roboto".into()), }; + *styles = Some(Style { render_command: StyleProp::Value(render_command), + width: StyleProp::Value(Units::Pixels(layout_size.0)), + height: StyleProp::Value(Units::Pixels(layout_size.1)), ..styles.clone().unwrap_or_default() }); } diff --git a/src/widgets/window.rs b/src/widgets/window.rs index cfbe04b..c866c6a 100644 --- a/src/widgets/window.rs +++ b/src/widgets/window.rs @@ -51,8 +51,8 @@ pub fn Window( top: StyleProp::Value(Units::Pixels(0.0)), bottom: StyleProp::Value(Units::Pixels(0.0)), padding_left: StyleProp::Value(Units::Pixels(5.0)), - padding_top: StyleProp::Value(Units::Stretch(1.0)), - padding_bottom: StyleProp::Value(Units::Stretch(1.0)), + // padding_top: StyleProp::Value(Units::Stretch(1.0)), + // padding_bottom: StyleProp::Value(Units::Stretch(1.0)), ..Style::default() }; -- GitLab