From f04e8ea677705f07ff897808934d8e5fc456d8ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?John=20K=C3=A5re=20Alsaker?= Date: Mon, 6 Nov 2023 21:48:56 +0100 Subject: [PATCH] Add tab navigation --- examples/counter/src/main.rs | 6 +- examples/themes/src/main.rs | 14 +- examples/widget-gallery/src/checkbox.rs | 6 +- src/context.rs | 20 ++- src/inspector.rs | 57 ++++++++- src/style.rs | 3 + src/view.rs | 163 ++++++++++-------------- src/window_handle.rs | 6 +- 8 files changed, 161 insertions(+), 114 deletions(-) diff --git a/examples/counter/src/main.rs b/examples/counter/src/main.rs index 762a0a92..f3046cbf 100644 --- a/examples/counter/src/main.rs +++ b/examples/counter/src/main.rs @@ -19,7 +19,7 @@ fn app_view() -> impl View { .padding(10.0) .background(Color::WHITE) .box_shadow_blur(5.0) - .focus_visible(|s| s.border(2.).border_color(Color::BLUE)) + .focus_visible(|s| s.outline(2.).outline_color(Color::BLUE)) .hover(|s| s.background(Color::LIGHT_GREEN)) .active(|s| s.color(Color::WHITE).background(Color::DARK_GREEN)) }) @@ -43,7 +43,7 @@ fn app_view() -> impl View { .border_radius(10.0) .padding(10.0) .margin_left(10.0) - .focus_visible(|s| s.border(2.).border_color(Color::BLUE)) + .focus_visible(|s| s.outline(2.).outline_color(Color::BLUE)) .hover(|s| s.background(Color::rgb8(244, 67, 54))) .active(|s| s.color(Color::WHITE).background(Color::RED)) }) @@ -61,7 +61,7 @@ fn app_view() -> impl View { .padding(10.0) .margin_left(10.0) .background(Color::LIGHT_BLUE) - .focus_visible(|s| s.border(2.).border_color(Color::BLUE)) + .focus_visible(|s| s.outline(2.).outline_color(Color::BLUE)) .disabled(|s| s.background(Color::LIGHT_GRAY)) .hover(|s| s.background(Color::LIGHT_YELLOW)) .active(|s| s.color(Color::WHITE).background(Color::YELLOW_GREEN)) diff --git a/examples/themes/src/main.rs b/examples/themes/src/main.rs index 8b67a4db..2d3d008b 100644 --- a/examples/themes/src/main.rs +++ b/examples/themes/src/main.rs @@ -3,7 +3,7 @@ use floem::{ keyboard::{Key, NamedKey}, peniko::Color, reactive::create_signal, - style::{Background, BorderColor, Style, TextColor, Transition}, + style::{Background, BorderColor, Outline, OutlineColor, Style, TextColor, Transition}, style_class, view::View, views::{label, stack, text, Decorators}, @@ -23,6 +23,11 @@ fn app_view() -> impl View { .transition(TextColor, Transition::linear(0.06)) .transition(BorderColor, Transition::linear(0.06)) .transition(Background, Transition::linear(0.06)) + .transition(Outline, Transition::linear(0.1)) + .focus_visible(|s| { + s.outline(2.0) + .outline_color(Color::WHITE.with_alpha_factor(0.7)) + }) .disabled(|s| { s.background(Color::DARK_GRAY.with_alpha_factor(0.1)) .border_color(Color::BLACK.with_alpha_factor(0.2)) @@ -55,6 +60,13 @@ fn app_view() -> impl View { .transition(TextColor, Transition::linear(0.3)) .transition(BorderColor, Transition::linear(0.3)) .transition(Background, Transition::linear(0.3)) + .transition(Outline, Transition::linear(0.2)) + .transition(OutlineColor, Transition::linear(0.2)) + .outline_color(Color::rgba8(131, 145, 123, 0)) + .focus_visible(|s| { + s.outline(10.0) + .outline_color(Color::rgb8(131, 145, 123).with_alpha_factor(0.3)) + }) .border_color(Color::rgb8(131, 145, 123)) .hover(|s| s.background(Color::rgb8(204, 209, 201))) .padding(8.0) diff --git a/examples/widget-gallery/src/checkbox.rs b/examples/widget-gallery/src/checkbox.rs index d7f4dc72..95d75954 100644 --- a/examples/widget-gallery/src/checkbox.rs +++ b/examples/widget-gallery/src/checkbox.rs @@ -23,6 +23,10 @@ pub fn checkbox_view() -> impl View { stack({ ( checkbox(is_checked) + .on_click(move |_| { + set_is_checked.update(|checked| *checked = !*checked); + true + }) .style(|s| s.focus_visible(|s| s.border(2.).border_color(Color::BLUE))), label(|| "Check me!"), ) @@ -36,12 +40,12 @@ pub fn checkbox_view() -> impl View { stack({ ( checkbox(is_checked) + .disabled(|| true) .style(|s| s.focus_visible(|s| s.border(2.).border_color(Color::BLUE))), label(|| "Check me!"), ) }) .style(|s| s.color(Color::GRAY)) - .disabled(|| true) .on_click(move |_| { set_is_checked.update(|checked| *checked = !*checked); true diff --git a/src/context.rs b/src/context.rs index dc016ba8..12554803 100644 --- a/src/context.rs +++ b/src/context.rs @@ -27,8 +27,8 @@ use crate::{ responsive::{GridBreakpoints, ScreenSizeBp}, style::{ Background, BorderBottom, BorderColor, BorderLeft, BorderRadius, BorderRight, BorderTop, - BuiltinStyle, CursorStyle, DisplayProp, LayoutProps, Style, StyleClassRef, StyleProp, - StyleSelector, StyleSelectors, ZIndex, + BuiltinStyle, CursorStyle, DisplayProp, LayoutProps, Outline, OutlineColor, Style, + StyleClassRef, StyleProp, StyleSelector, StyleSelectors, ZIndex, }, unit::PxPct, view::{paint_bg, paint_border, paint_outline, View}, @@ -57,6 +57,8 @@ prop_extracter! { pub border_bottom: BorderBottom, pub border_radius: BorderRadius, + pub outline: Outline, + pub outline_color: OutlineColor, pub border_color: BorderColor, pub background: Background, } @@ -548,7 +550,9 @@ impl AppState { pub(crate) fn clear_focus(&mut self) { if let Some(old_id) = self.focus { // To remove the styles applied by the Focus selector - if self.has_style_for_sel(old_id, StyleSelector::Focus) { + if self.has_style_for_sel(old_id, StyleSelector::Focus) + || self.has_style_for_sel(old_id, StyleSelector::FocusVisible) + { self.request_style(old_id); } } @@ -563,6 +567,12 @@ impl AppState { self.focus = Some(id); self.keyboard_navigation = keyboard_navigation; + + if self.has_style_for_sel(id, StyleSelector::Focus) + || self.has_style_for_sel(id, StyleSelector::FocusVisible) + { + self.request_style(id); + } } pub(crate) fn has_style_for_sel(&mut self, id: Id, selector_kind: StyleSelector) -> bool { @@ -1507,7 +1517,7 @@ impl<'a> PaintCx<'a> { view.paint(self); paint_border(self, &view_style_props, size); - paint_outline(self, &style, size) + paint_outline(self, &view_style_props, size) } let mut drag_set_to_none = false; @@ -1554,7 +1564,7 @@ impl<'a> PaintCx<'a> { view.paint(self); paint_border(self, &view_style_props, size); - paint_outline(self, &style, size); + paint_outline(self, &view_style_props, size); self.restore(); } diff --git a/src/inspector.rs b/src/inspector.rs index d6cb4639..6cc7530a 100644 --- a/src/inspector.rs +++ b/src/inspector.rs @@ -5,8 +5,8 @@ use crate::id::Id; use crate::style::{Style, StyleMapValue}; use crate::view::{view_children, View}; use crate::views::{ - dyn_container, empty, img_dynamic, scroll, stack, static_label, static_list, text, v_stack, - Decorators, Label, + dyn_container, empty, h_stack, img_dynamic, scroll, stack, static_label, static_list, text, + v_stack, Decorators, Label, }; use crate::window::WindowConfig; use crate::{new_window, style}; @@ -34,6 +34,8 @@ pub struct CapturedView { children: Vec>, direct_style: Style, requested_changes: ChangeFlags, + keyboard_navigable: bool, + focused: bool, } impl CapturedView { @@ -42,8 +44,9 @@ impl CapturedView { let layout = app_state.get_layout_rect(id); let taffy = app_state.get_layout(id).unwrap(); let computed_style = app_state.get_computed_style(id).clone(); + let keyboard_navigable = app_state.keyboard_navigable.contains(&id); + let focused = app_state.focus == Some(id); let state = app_state.view_state(id); - let clipped = layout.intersect(clip); Self { id, @@ -53,6 +56,8 @@ impl CapturedView { clipped, direct_style: computed_style, requested_changes: state.requested_changes, + keyboard_navigable, + focused, children: view_children(view) .into_iter() .map(|view| Rc::new(CapturedView::capture(view, app_state, clipped))) @@ -119,6 +124,46 @@ impl CaptureState { } } +fn captured_view_name(view: &CapturedView) -> impl View { + let name = static_label(view.name.clone()); + let id = text(view.id.to_raw()).style(|s| { + s.margin_right(5.0) + .background(Color::BLACK.with_alpha_factor(0.02)) + .border(1.0) + .border_radius(5.0) + .border_color(Color::BLACK.with_alpha_factor(0.07)) + .padding(3.0) + .padding_top(0.0) + .padding_bottom(0.0) + .font_size(12.0) + .color(Color::BLACK.with_alpha_factor(0.6)) + }); + let tab: Box = if view.focused { + Box::new(text("Focus").style(|s| { + s.margin_right(5.0) + .background(Color::rgb8(63, 81, 101).with_alpha_factor(0.6)) + .border_radius(5.0) + .padding(1.0) + .font_size(10.0) + .color(Color::WHITE.with_alpha_factor(0.8)) + })) + } else if view.keyboard_navigable { + Box::new(text("Tab").style(|s| { + s.margin_right(5.0) + .background(Color::rgb8(204, 217, 221).with_alpha_factor(0.4)) + .border(1.0) + .border_radius(5.0) + .border_color(Color::BLACK.with_alpha_factor(0.07)) + .padding(1.0) + .font_size(10.0) + .color(Color::BLACK.with_alpha_factor(0.4)) + })) + } else { + Box::new(empty()) + }; + h_stack((id, tab, name)).style(|s| s.items_center()) +} + // Outlined to reduce stack usage. #[inline(never)] fn captured_view_no_children( @@ -128,7 +173,7 @@ fn captured_view_no_children( highlighted: RwSignal>, ) -> Box { let offset = depth as f64 * 14.0; - let name = static_label(view.name.clone()); + let name = captured_view_name(view); let height = 20.0; let id = view.id; @@ -174,7 +219,7 @@ fn captured_view_with_children( children: Vec>, ) -> Box { let offset = depth as f64 * 14.0; - let name = static_label(view.name.clone()); + let name = captured_view_name(view); let height = 20.0; let id = view.id; @@ -354,6 +399,7 @@ fn selected_view(capture: &Rc, selected: RwSignal>) -> impl move |current| { if let Some(view) = current.and_then(|id| capture.root.find(id)) { let name = info("Type", view.name.clone()); + let id = info("Id", view.id.to_raw().to_string()); let count = info("Child Count", format!("{}", view.children.len())); let beyond = |view: f64, window| { if view > window { @@ -547,6 +593,7 @@ fn selected_view(capture: &Rc, selected: RwSignal>) -> impl Box::new( v_stack(( name, + id, count, x, y, diff --git a/src/style.rs b/src/style.rs index f54f3f05..b1f6ed4b 100644 --- a/src/style.rs +++ b/src/style.rs @@ -102,6 +102,9 @@ impl StylePropValue for Px { fn debug_view(&self) -> Option> { Some(Box::new(text(format!("{} px", self.0)))) } + fn interpolate(&self, other: &Self, value: f64) -> Option { + self.0.interpolate(&other.0, value).map(Px) + } } impl StylePropValue for PxPctAuto { fn debug_view(&self) -> Option> { diff --git a/src/view.rs b/src/view.rs index bf94a112..9d8312b0 100644 --- a/src/view.rs +++ b/src/view.rs @@ -92,7 +92,7 @@ use crate::{ context::{AppState, EventCx, LayoutCx, PaintCx, StyleCx, UpdateCx, ViewStyleProps}, event::Event, id::Id, - style::{BoxShadowProp, Outline, OutlineColor, Style, StyleClassRef}, + style::{BoxShadowProp, Style, StyleClassRef}, }; pub trait View { @@ -298,15 +298,19 @@ fn paint_box_shadow(cx: &mut PaintCx, style: &Style, rect: Rect, rect_radius: Op } } -pub(crate) fn paint_outline(cx: &mut PaintCx, style: &Style, size: Size) { - let outline = style.get(Outline).0; +pub(crate) fn paint_outline(cx: &mut PaintCx, style: &ViewStyleProps, size: Size) { + let outline = style.outline().0; if outline == 0. { // TODO: we should warn! when outline is < 0 return; } let half = outline / 2.0; let rect = size.to_rect().inflate(half, half); - cx.stroke(&rect, style.get(OutlineColor), outline); + cx.stroke( + &rect.to_rounded_rect(style.border_radius().0 + half), + style.outline_color(), + outline, + ); } pub(crate) fn paint_border(cx: &mut PaintCx, style: &ViewStyleProps, size: Size) { @@ -379,29 +383,38 @@ pub(crate) fn view_children(view: &dyn View) -> Vec<&dyn View> { /// Tab navigation finds the next or previous view with the `keyboard_navigatable` status in the tree. #[allow(dead_code)] pub(crate) fn view_tab_navigation(root_view: &dyn View, app_state: &mut AppState, backwards: bool) { - let start = app_state.focus.unwrap_or(root_view.id()); - println!("start id is {start:?}"); + let start = app_state + .focus + .filter(|id| id.id_path().is_some()) + .unwrap_or(root_view.id()); let tree_iter = |id: Id| { if backwards { - view_tree_previous(root_view, &id, app_state) - .unwrap_or(view_nested_last_child(root_view).id()) + view_tree_previous(root_view, &id) + .unwrap_or_else(|| view_nested_last_child(root_view).id()) } else { - view_tree_next(root_view, &id, app_state).unwrap_or(root_view.id()) + view_tree_next(root_view, &id).unwrap_or(root_view.id()) } }; + let hidden = |app_state: &mut AppState, id: Id| { + id.id_path() + .unwrap() + .dispatch() + .iter() + .any(|id| app_state.is_hidden(*id)) + }; + let mut new_focus = tree_iter(start); - println!("new focus is {new_focus:?}"); while new_focus != start - && (!app_state.keyboard_navigable.contains(&new_focus) || app_state.is_disabled(&new_focus)) + && (!app_state.keyboard_navigable.contains(&new_focus) + || app_state.is_disabled(&new_focus) + || hidden(app_state, new_focus)) { new_focus = tree_iter(new_focus); - println!("new focus is {new_focus:?}"); } app_state.clear_focus(); app_state.update_focus(new_focus, true); - println!("Tab to {new_focus:?}"); } fn view_filtered_children<'a>(view: &'a dyn View, id_path: &[Id]) -> Vec<&'a dyn View> { @@ -422,118 +435,76 @@ fn view_filtered_children<'a>(view: &'a dyn View, id_path: &[Id]) -> Vec<&'a dyn } /// Get the next item in the tree, either the first child or the next sibling of this view or of the first parent view -fn view_tree_next(root_view: &dyn View, id: &Id, app_state: &AppState) -> Option { - let id_path = id.id_path()?; - - println!("id is {id:?}"); - println!("id path is {:?}", id_path.0); - - let children = view_filtered_children(root_view, &id_path.0); +fn view_tree_next(root_view: &dyn View, id: &Id) -> Option { + let id_path = id.id_path().unwrap(); - println!( - "children is {:?}", - children.iter().map(|v| v.id()).collect::>() - ); - for child in children { - if app_state.is_hidden(child.id()) { - continue; - } + if let Some(child) = view_filtered_children(root_view, id_path.dispatch()) + .into_iter() + .next() + { return Some(child.id()); } let mut ancestor = *id; loop { - let id_path = ancestor.id_path()?; - println!("try to find sibling for {:?}", id_path.0); - if let Some(next_sibling) = view_next_sibling(root_view, &id_path.0, app_state) { - println!("next sibling is {:?}", next_sibling.id()); + let id_path = ancestor.id_path().unwrap(); + if id_path.dispatch().is_empty() { + return None; + } + if let Some(next_sibling) = view_next_sibling(root_view, id_path.dispatch()) { return Some(next_sibling.id()); } ancestor = ancestor.parent()?; - println!("go to ancestor {ancestor:?}"); } } /// Get the id of the view after this one (but with the same parent and level of nesting) -fn view_next_sibling<'a>( - view: &'a dyn View, - id_path: &[Id], - app_state: &AppState, -) -> Option<&'a dyn View> { - let id = id_path[0]; - let id_path = &id_path[1..]; - if id == view.id() { - if app_state.is_hidden(id) { - return None; - } +fn view_next_sibling<'a>(root_view: &'a dyn View, id_path: &[Id]) -> Option<&'a dyn View> { + let id = *id_path.last().unwrap(); + let parent = &id_path[0..(id_path.len() - 1)]; - if id_path.is_empty() { - return None; - } + if parent.is_empty() { + // We're the root, which has no sibling + return None; + } - if id_path.len() == 1 { - println!("id is {id:?} id_path is {:?}", id_path); - let child_id = id_path[0]; - let children = view_children(view); - let pos = children.iter().position(|v| v.id() == child_id); - if let Some(pos) = pos { - if children.len() > 1 && pos < children.len() - 1 { - return Some(children[pos + 1]); - } - } - return None; - } + let children = view_filtered_children(root_view, parent); + let pos = children.iter().position(|v| v.id() == id).unwrap(); - if let Some(child) = view.child(id_path[0]) { - return view_next_sibling(child, id_path, app_state); - } + if pos + 1 < children.len() { + Some(children[pos + 1]) + } else { + None } - None } /// Get the next item in the tree, the deepest last child of the previous sibling of this view or the parent -fn view_tree_previous(root_view: &dyn View, id: &Id, app_state: &AppState) -> Option { - let id_path = id.id_path()?; +fn view_tree_previous(root_view: &dyn View, id: &Id) -> Option { + let id_path = id.id_path().unwrap(); - view_previous_sibling(root_view, &id_path.0, app_state) + view_previous_sibling(root_view, id_path.dispatch()) .map(|view| view_nested_last_child(view).id()) - .or_else(|| id.parent()) + .or_else(|| (root_view.id() != *id).then_some(id.parent().unwrap())) } /// Get the id of the view before this one (but with the same parent and level of nesting) -fn view_previous_sibling<'a>( - view: &'a dyn View, - id_path: &[Id], - app_state: &AppState, -) -> Option<&'a dyn View> { - let id = id_path[0]; - let id_path = &id_path[1..]; - if id == view.id() { - if app_state.is_hidden(id) { - return None; - } +fn view_previous_sibling<'a>(root_view: &'a dyn View, id_path: &[Id]) -> Option<&'a dyn View> { + let id = *id_path.last().unwrap(); + let parent = &id_path[0..(id_path.len() - 1)]; - if id_path.is_empty() { - return None; - } + if parent.is_empty() { + // We're the root, which has no sibling + return None; + } - if id_path.len() == 1 { - let child_id = id_path[0]; - let children = view_children(view); - let pos = children.iter().position(|v| v.id() == child_id); - if let Some(pos) = pos { - if pos > 0 { - return Some(children[pos - 1]); - } - } - return None; - } + let children = view_filtered_children(root_view, parent); + let pos = children.iter().position(|v| v.id() == id).unwrap(); - if let Some(child) = view.child(id_path[0]) { - return view_previous_sibling(child, id_path, app_state); - } + if pos > 0 { + Some(children[pos - 1]) + } else { + None } - None } pub(crate) fn view_children_set_parent_id(view: &dyn View) { diff --git a/src/window_handle.rs b/src/window_handle.rs index 93a85958..fb7c42cb 100644 --- a/src/window_handle.rs +++ b/src/window_handle.rs @@ -37,7 +37,7 @@ use crate::{ CENTRAL_UPDATE_MESSAGES, CURRENT_RUNNING_VIEW_HANDLE, DEFERRED_UPDATE_MESSAGES, UPDATE_MESSAGES, }, - view::{view_children_set_parent_id, View}, + view::{view_children_set_parent_id, view_tab_navigation, View}, }; /// The top-level window handle that owns the winit Window. @@ -190,8 +190,8 @@ impl WindowHandle { if !processed { if let Event::KeyDown(KeyEvent { key, modifiers }) = &event { if key.logical_key == Key::Named(NamedKey::Tab) { - let _backwards = modifiers.contains(ModifiersState::SHIFT); - // view_tab_navigation(&self.view, cx.app_state, backwards); + let backwards = modifiers.contains(ModifiersState::SHIFT); + view_tab_navigation(&self.view, cx.app_state, backwards); // view_debug_tree(&self.view); } else if let Key::Character(character) = &key.logical_key { // 'I' displays some debug information