diff --git a/examples/gui/platform/Cargo.toml b/examples/gui/platform/Cargo.toml index b82712e562..dee9bee179 100644 --- a/examples/gui/platform/Cargo.toml +++ b/examples/gui/platform/Cargo.toml @@ -4,7 +4,6 @@ version = "0.1.0" authors = ["The Roc Contributors"] license = "UPL-1.0" edition = "2018" -links = "app" # Needed to be able to run on non-Windows systems for some reason. Without this, cargo panics with: # diff --git a/examples/gui/platform/build.rs b/examples/gui/platform/build.rs deleted file mode 100644 index 73159e387c..0000000000 --- a/examples/gui/platform/build.rs +++ /dev/null @@ -1,4 +0,0 @@ -fn main() { - println!("cargo:rustc-link-lib=dylib=app"); - println!("cargo:rustc-link-search=."); -} diff --git a/examples/gui/platform/src/focus.rs b/examples/gui/platform/src/focus.rs new file mode 100644 index 0000000000..4d844fdd6a --- /dev/null +++ b/examples/gui/platform/src/focus.rs @@ -0,0 +1,148 @@ +use crate::roc::{RocElem, RocElemTag}; + +#[derive(Debug, PartialEq, Eq)] +pub struct Focus { + focused: *const RocElem, + focused_ancestors: Vec<(*const RocElem, usize)>, +} + +impl Default for Focus { + fn default() -> Self { + Self { + focused: std::ptr::null(), + focused_ancestors: Vec::new(), + } + } +} + +impl Focus { + pub fn focused_elem(&self) -> *const RocElem { + self.focused + } + + /// e.g. the user pressed Tab. + pub fn advance(&mut self, root: &RocElem) { + if self.focused.is_null() { + // Nothing was focused in the first place, so try to focus the root. + if root.is_focusable() { + self.focused = root as *const RocElem; + self.focused_ancestors = Vec::new(); + } else if let Some((new_ptr, new_ancestors)) = + Self::next_focusable_sibling(root, None, None) + { + // If the root itself is not focusable, use its next focusable sibling. + self.focused = new_ptr; + self.focused_ancestors = new_ancestors; + } + + // Regardless of whether we found a focusable Elem, we're done. + return; + } + + let focused = unsafe { &*self.focused }; + + while let Some((ancestor_ptr, index)) = self.focused_ancestors.pop() { + let ancestor = unsafe { &*ancestor_ptr }; + + // TODO FIXME - right now this will re-traverse a lot of ground! To prevent this, + // we should remember past indices searched, and tell the ancestors "hey stop searching when" + // you reach these indices, because they were already covered previously. + // One potentially easy way to do this: pass a min_index and max_index, and only look between those! + // + // Related idea: instead of doing .pop() here, iterate normally so we can `break;` after storing + // `new_ancestors = Some(next_ancestors);` - this way, we still have access to the full ancestry, and + // can maybe even pass it in to make it clear what work has already been done! + if let Some((new_ptr, new_ancestors)) = + Self::next_focusable_sibling(focused, Some(ancestor), Some(index)) + { + debug_assert!( + !new_ptr.is_null(), + "next_focusable returned a null Elem pointer!" + ); + + // We found the next element to focus, so record that. + self.focused = new_ptr; + + // We got a path to the new focusable's ancestor(s), so add them to the path. + // (This may restore some of the ancestors we've been .pop()-ing as we iterated.) + self.focused_ancestors.extend(new_ancestors); + + return; + } + + // Need to write a bunch of tests for this, especially tests of focus wrapping around - e.g. + // what happens if it wraps around to a sibling? What happens if it wraps around to something + // higher up the tree? Lower down the tree? What if nothing is focusable? + // A separate question: what if we should have a separate text-to-speech concept separate from focus? + } + } + + /// Return the next focusable sibling element after this one. + /// If this element has no siblings, or no *next* sibling after the given index + /// (e.g. the given index refers to the last element in a Row element), return None. + fn next_focusable_sibling( + elem: &RocElem, + ancestor: Option<&RocElem>, + opt_index: Option, + ) -> Option<(*const RocElem, Vec<(*const RocElem, usize)>)> { + use RocElemTag::*; + + match elem.tag() { + Button | Text => None, + Row | Col => { + let children = unsafe { &elem.entry().row_or_col.children.as_slice() }; + let iter = match opt_index { + Some(focus_index) => children[0..focus_index].iter(), + None => children.iter(), + }; + + for child in iter { + if let Some(focused) = Self::next_focusable_sibling(child, ancestor, None) { + return Some(focused); + } + } + + None + } + } + } +} + +#[test] +fn next_focus_button_root() { + use crate::roc::{ButtonStyles, RocElem}; + + let child = RocElem::text(""); + let root = RocElem::button(ButtonStyles::default(), child); + let mut focus = Focus::default(); + + // At first, nothing should be focused. + assert_eq!(focus.focused_elem(), std::ptr::null()); + + focus.advance(&root); + + // Buttons should be focusable, so advancing focus should give the button focus. + assert_eq!(focus.focused_elem(), &root as *const RocElem); + + // Since the button is at the root, advancing again should maintain focus on it. + focus.advance(&root); + assert_eq!(focus.focused_elem(), &root as *const RocElem); +} + +#[test] +fn next_focus_text_root() { + let root = RocElem::text(""); + let mut focus = Focus::default(); + + // At first, nothing should be focused. + assert_eq!(focus.focused_elem(), std::ptr::null()); + + focus.advance(&root); + + // Text should not be focusable, so advancing focus should have no effect here. + assert_eq!(focus.focused_elem(), std::ptr::null()); + + // Just to double-check, advancing a second time should not change this. + focus.advance(&root); + assert_eq!(focus.focused_elem(), std::ptr::null()); +} diff --git a/examples/gui/platform/src/graphics/colors.rs b/examples/gui/platform/src/graphics/colors.rs index e0932a1d69..3ec448413f 100644 --- a/examples/gui/platform/src/graphics/colors.rs +++ b/examples/gui/platform/src/graphics/colors.rs @@ -3,7 +3,7 @@ use palette::{FromColor, Hsv, Srgb}; /// This order is optimized for what Roc will send #[repr(C)] -#[derive(Copy, Clone, Debug, PartialEq)] +#[derive(Copy, Clone, Debug, PartialEq, Default)] pub struct Rgba { a: f32, b: f32, diff --git a/examples/gui/platform/src/gui.rs b/examples/gui/platform/src/gui.rs index 0e6bc8b24b..d811eeb552 100644 --- a/examples/gui/platform/src/gui.rs +++ b/examples/gui/platform/src/gui.rs @@ -1,4 +1,5 @@ use crate::{ + focus::Focus, graphics::{ colors::Rgba, lowlevel::buffer::create_rect_buffers, @@ -231,9 +232,15 @@ fn run_event_loop(title: &str, root: RocElem) -> Result<(), Box> { // wgpu::LoadOp::Load, // ); - // TODO use with_capacity based on some heuristic + let focus_ancestry: Vec<(*const RocElem, usize)> = Vec::new(); // TODO test that root node can get focus! + let focused_elem: *const RocElem = match focus_ancestry.first() { + Some((ptr_ref, _)) => *ptr_ref, + None => std::ptr::null(), + }; + let (_bounds, drawable) = to_drawable( &root, + focused_elem, Bounds { width: size.width as f32, height: size.height as f32, @@ -496,18 +503,23 @@ fn draw( } } +/// focused_elem is the currently-focused element (or NULL if nothing has the focus) fn to_drawable( elem: &RocElem, + focused_elem: *const RocElem, bounds: Bounds, glyph_brush: &mut GlyphBrush<()>, ) -> (Bounds, Drawable) { use RocElemTag::*; + let is_focused = focused_elem == elem as *const RocElem; + match elem.tag() { Button => { let button = unsafe { &elem.entry().button }; let styles = button.styles; - let (child_bounds, child_drawable) = to_drawable(&*button.child, bounds, glyph_brush); + let (child_bounds, child_drawable) = + to_drawable(&*button.child, focused_elem, bounds, glyph_brush); let button_drawable = Drawable { bounds: child_bounds, @@ -574,7 +586,8 @@ fn to_drawable( let mut offset_entries = Vec::with_capacity(row.children.len()); for child in row.children.as_slice().iter() { - let (child_bounds, child_drawable) = to_drawable(&child, bounds, glyph_brush); + let (child_bounds, child_drawable) = + to_drawable(&child, focused_elem, bounds, glyph_brush); offset_entries.push((offset, child_drawable)); @@ -603,7 +616,8 @@ fn to_drawable( let mut offset_entries = Vec::with_capacity(col.children.len()); for child in col.children.as_slice().iter() { - let (child_bounds, child_drawable) = to_drawable(&child, bounds, glyph_brush); + let (child_bounds, child_drawable) = + to_drawable(&child, focused_elem, bounds, glyph_brush); offset_entries.push((offset, child_drawable)); diff --git a/examples/gui/platform/src/lib.rs b/examples/gui/platform/src/lib.rs index 65e55452b0..9f78d35fcf 100644 --- a/examples/gui/platform/src/lib.rs +++ b/examples/gui/platform/src/lib.rs @@ -1,3 +1,4 @@ +mod focus; mod graphics; mod gui; mod rects_and_texts; diff --git a/examples/gui/platform/src/roc.rs b/examples/gui/platform/src/roc.rs index 034181deed..005ca4e6ba 100644 --- a/examples/gui/platform/src/roc.rs +++ b/examples/gui/platform/src/roc.rs @@ -62,16 +62,106 @@ impl RocElem { } pub fn entry(&self) -> &RocElemEntry { + unsafe { &*self.entry_ptr() } + } + + pub fn entry_ptr(&self) -> *const RocElemEntry { // On a 64-bit system, the last 3 bits of the pointer store the tag let cleared = self.entry as usize & !0b111; - unsafe { &*(cleared as *const RocElemEntry) } + cleared as *const RocElemEntry + } + + fn diff(self, other: RocElem, patches: &mut Vec<(usize, Patch)>, index: usize) { + use RocElemTag::*; + + let tag = self.tag(); + + if tag != other.tag() { + // They were totally different elem types! + + // TODO should we handle Row -> Col or Col -> Row differently? + // Elm doesn't: https://github.com/elm/virtual-dom/blob/5a5bcf48720bc7d53461b3cd42a9f19f119c5503/src/Elm/Kernel/VirtualDom.js#L714 + return; + } + + match tag { + Button => unsafe { + let button_self = &*self.entry().button; + let button_other = &*other.entry().button; + + // TODO compute a diff and patch for the button + }, + Text => unsafe { + let str_self = &*self.entry().text; + let str_other = &*other.entry().text; + + if str_self != str_other { + todo!("fix this"); + // let roc_str = other.entry().text; + // let patch = Patch::Text(ManuallyDrop::into_inner(roc_str)); + + // patches.push((index, patch)); + } + }, + Row => unsafe { + let children_self = &self.entry().row_or_col.children; + let children_other = &other.entry().row_or_col.children; + + // TODO diff children + }, + Col => unsafe { + let children_self = &self.entry().row_or_col.children; + let children_other = &other.entry().row_or_col.children; + + // TODO diff children + }, + } + } + + pub fn is_focusable(&self) -> bool { + use RocElemTag::*; + + match self.tag() { + Button => true, + Text | Row | Col => false, + } + } + + pub fn button(styles: ButtonStyles, child: RocElem) -> RocElem { + let button = RocButton { + child: ManuallyDrop::new(child), + styles, + }; + let entry = RocElemEntry { + button: ManuallyDrop::new(button), + }; + + Self::elem_from_tag(entry, RocElemTag::Button) + } + + pub fn text>(into_roc_str: T) -> RocElem { + let entry = RocElemEntry { + text: ManuallyDrop::new(into_roc_str.into()), + }; + + Self::elem_from_tag(entry, RocElemTag::Text) + } + + fn elem_from_tag(entry: RocElemEntry, tag: RocElemTag) -> Self { + let entry_box = Box::new(entry); + let entry_ptr = entry_box.as_ref() as *const RocElemEntry; + let tagged_ptr = entry_ptr as usize | tag as usize; + + Self { + entry: tagged_ptr as *const RocElemEntry, + } } } #[repr(u8)] #[allow(unused)] // This is actually used, just via a mem::transmute from u8 -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum RocElemTag { Button = 0, Col, @@ -134,7 +224,7 @@ unsafe impl ReferenceCount for RocElem { } #[repr(C)] -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, Default)] pub struct ButtonStyles { pub bg_color: Rgba, pub border_color: Rgba, @@ -148,3 +238,7 @@ pub union RocElemEntry { pub text: ManuallyDrop, pub row_or_col: ManuallyDrop, } + +enum Patch { + Text(RocStr), +}