mirror of
https://github.com/roc-lang/roc.git
synced 2024-09-22 00:09:33 +03:00
First pass at Focus
This commit is contained in:
parent
9bf513b55c
commit
8302b732b8
@ -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:
|
||||
#
|
||||
|
@ -1,4 +0,0 @@
|
||||
fn main() {
|
||||
println!("cargo:rustc-link-lib=dylib=app");
|
||||
println!("cargo:rustc-link-search=.");
|
||||
}
|
148
examples/gui/platform/src/focus.rs
Normal file
148
examples/gui/platform/src/focus.rs
Normal 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());
|
||||
}
|
@ -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,
|
||||
|
@ -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));
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
mod focus;
|
||||
mod graphics;
|
||||
mod gui;
|
||||
mod rects_and_texts;
|
||||
|
@ -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),
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user