First pass at Focus

This commit is contained in:
Richard Feldman 2022-02-28 17:35:17 -05:00
parent 9bf513b55c
commit 8302b732b8
No known key found for this signature in database
GPG Key ID: 7E4127D1E4241798
7 changed files with 265 additions and 13 deletions

View File

@ -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:
#

View File

@ -1,4 +0,0 @@
fn main() {
println!("cargo:rustc-link-lib=dylib=app");
println!("cargo:rustc-link-search=.");
}

View File

@ -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<usize>,
) -> 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());
}

View File

@ -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,

View File

@ -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<dyn Error>> {
// 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));

View File

@ -1,3 +1,4 @@
mod focus;
mod graphics;
mod gui;
mod rects_and_texts;

View File

@ -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<T: Into<RocStr>>(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<RocStr>,
pub row_or_col: ManuallyDrop<RocRowOrCol>,
}
enum Patch {
Text(RocStr),
}