diff --git a/examples/tabs/tab.rs b/examples/tabs/tab.rs index 63b34d926c1b8cf9e0a087abeeb6332e985cd8e3..5af1081fc44bf5eb7fc63c285064321c036f15db 100644 --- a/examples/tabs/tab.rs +++ b/examples/tabs/tab.rs @@ -2,7 +2,7 @@ use kayak_ui::{ core::{ render_command::RenderCommand, styles::{LayoutType, Style, StyleProp, Units}, - Bound, EventType, Handler, KeyCode, OnEvent, rsx, use_state, widget, + Bound, EventType, OnEvent, rsx, use_state, widget, }, widgets::{Background, Text}, }; @@ -16,17 +16,17 @@ enum TabHoverState { Active, } +/// The actual tab, displayed in a [TabBar](crate::tab_bar::TabBar) #[widget] -pub fn Tab(context: &mut KayakContext, content: String, selected: bool, on_request_remove: Handler) { +pub fn Tab(context: &mut KayakContext, content: String, selected: bool) { let theme = context.create_consumer::<TabTheme>().unwrap_or_default(); + let (focus_state, set_focus_state, ..) = use_state!(false); let (hover_state, set_hover_state, ..) = use_state!(TabHoverState::None); match hover_state { TabHoverState::Inactive if selected => set_hover_state(TabHoverState::Active), TabHoverState::Active if !selected => set_hover_state(TabHoverState::Inactive), _ => {} }; - let (focus_state, set_focus_state, ..) = use_state!(false); - let (is_exit_hovered, set_is_exit_hovered, ..) = use_state!(false); let event_handler = OnEvent::new(move |_, event| { match event.event_type { @@ -50,36 +50,6 @@ pub fn Tab(context: &mut KayakContext, content: String, selected: bool, on_reque } }); - let exit_btn_event_handler = OnEvent::new(move |_, event| { - match event.event_type { - EventType::Hover => { - set_is_exit_hovered(true); - } - EventType::MouseOut => { - set_is_exit_hovered(false); - } - EventType::Focus => { - set_is_exit_hovered(true); - } - EventType::Blur => { - set_is_exit_hovered(false); - } - EventType::Click => { - // Stop propagation so we don't select a deleted tab! - event.stop_propagation(); - on_request_remove.call(()); - } - EventType::KeyDown(evt) => { - if evt.key() == KeyCode::Return || evt.key() == KeyCode::Space { - // Stop propagation so we don't select a deleted tab! - event.stop_propagation(); - on_request_remove.call(()); - } - } - _ => {} - } - }); - let tab_color = match hover_state { TabHoverState::None if selected => theme.get().active_tab.normal, TabHoverState::None => theme.get().inactive_tab.normal, @@ -96,7 +66,6 @@ pub fn Tab(context: &mut KayakContext, content: String, selected: bool, on_reque ..Default::default() }; - let border_width = Units::Pixels(2.0); let border_styles = Style { background_color: if focus_state { @@ -125,32 +94,6 @@ pub fn Tab(context: &mut KayakContext, content: String, selected: bool, on_reque ..Default::default() }; - let exit_styles = Style { - background_color: if is_exit_hovered { - let mut darkened = theme.get().inactive_tab.hovered; - darkened.r -= 0.025; - darkened.g -= 0.025; - darkened.b -= 0.025; - StyleProp::Value(darkened) - } else { - StyleProp::Value(tab_color) - }, - width: StyleProp::Value(Units::Pixels(theme.get().tab_height - 4.0)), - height: StyleProp::Value(Units::Pixels(theme.get().tab_height - 4.0)), - top: StyleProp::Value(Units::Stretch(0.175)), - bottom: StyleProp::Value(Units::Stretch(1.0)), - left: StyleProp::Value(Units::Stretch(1.0)), - ..Default::default() - }; - - let exit_text_styles = Style { - left: StyleProp::Value(Units::Stretch(1.0)), - right: StyleProp::Value(Units::Stretch(1.0)), - top: StyleProp::Value(Units::Stretch(0.35)), - bottom: StyleProp::Value(Units::Stretch(1.0)), - ..Default::default() - }; - self.styles = Some(Style { render_command: StyleProp::Value(RenderCommand::Layout), height: StyleProp::Value(Units::Pixels(theme.get().tab_height)), @@ -162,9 +105,6 @@ pub fn Tab(context: &mut KayakContext, content: String, selected: bool, on_reque <Background focusable={Some(true)} on_event={Some(event_handler)} styles={Some(border_styles)}> <Background styles={Some(bg_styles)}> <Text content={content} size={12.0} styles={Some(text_styles)} /> - <Background focusable={Some(true)} on_event={Some(exit_btn_event_handler)} styles={Some(exit_styles)}> - <Text content={"X".to_string()} size={8.0} styles={Some(exit_text_styles)} /> - </Background> </Background> </Background> } diff --git a/examples/tabs/tab_bar.rs b/examples/tabs/tab_bar.rs index 2d9cecedbc6771d0d8e003bebdfe1bf686feb931..12608a8544d99a0785fb208d45ac3878db0eb618 100644 --- a/examples/tabs/tab_bar.rs +++ b/examples/tabs/tab_bar.rs @@ -2,18 +2,18 @@ use kayak_ui::{ core::{ styles::{LayoutType, Style, StyleProp, Units}, Bound, constructor, EventType, Handler, KeyCode, OnEvent, - rsx, use_state, VecTracker, widget, + rsx, VecTracker, widget, }, - widgets::{Background, Element, Text}, + widgets::Background, }; use crate::tab::Tab; use crate::TabTheme; +/// A widget displaying a collection of tabs in a horizontal bar #[widget] -pub fn TabBar(context: &mut KayakContext, tabs: Vec<String>, selected: usize, on_select_tab: Handler<usize>, on_add_tab: Handler, on_remove_tab: Handler<usize>) { +pub fn TabBar(context: &mut KayakContext, tabs: Vec<String>, selected: usize, on_select_tab: Handler<usize>) { let theme = context.create_consumer::<TabTheme>().unwrap_or_default(); - let (is_add_hovered, set_is_add_hovered, ..) = use_state!(false); let tabs = tabs.iter().enumerate().map(|(index, tab)| { let on_select = on_select_tab.clone(); @@ -24,6 +24,7 @@ pub fn TabBar(context: &mut KayakContext, tabs: Vec<String>, selected: usize, on } EventType::KeyDown(evt) => { if evt.key() == KeyCode::Return || evt.key() == KeyCode::Space { + // We want the focused tab to also be selected by `Enter` or `Space` on_select.call(index); } } @@ -31,59 +32,11 @@ pub fn TabBar(context: &mut KayakContext, tabs: Vec<String>, selected: usize, on } }); - let on_remove = on_remove_tab.clone(); - let on_request_remove = Handler::new(move |_| { - on_remove.call(index); - }); - constructor! { - <Tab content={tab.clone()} on_event={Some(tab_event_handler.clone())} selected={selected == index} on_request_remove={on_request_remove} /> + <Tab content={tab.clone()} on_event={Some(tab_event_handler.clone())} selected={selected == index} /> } }).collect::<Vec<_>>(); - let add_btn_event_handler = OnEvent::new(move |_, event| match event.event_type { - EventType::Hover => { - set_is_add_hovered(true); - } - EventType::MouseOut => { - set_is_add_hovered(false); - } - EventType::Focus => { - set_is_add_hovered(true); - } - EventType::Blur => { - set_is_add_hovered(false); - } - EventType::Click => { - on_add_tab.call(()); - } - EventType::KeyDown(evt) => { - if evt.key() == KeyCode::Return || evt.key() == KeyCode::Space { - on_add_tab.call(()); - } - } - _ => {} - }); - - let add_btn_styles = Style { - height: StyleProp::Value(Units::Pixels(theme.get().tab_height)), - width: StyleProp::Value(Units::Pixels(theme.get().tab_height)), - border_radius: StyleProp::Value((10.0, 10.0, 10.0, 10.0)), - ..Default::default() - }; - let add_btn_text_styles = Style { - color: if is_add_hovered { - StyleProp::Value(theme.get().text.hovered) - } else { - StyleProp::Value(theme.get().text.normal) - }, - left: StyleProp::Value(Units::Stretch(1.0)), - right: StyleProp::Value(Units::Stretch(1.0)), - height: StyleProp::Value(Units::Percentage(100.0)), - width: StyleProp::Value(Units::Percentage(100.0)), - ..Default::default() - }; - let background_styles = Style { layout_type: StyleProp::Value(LayoutType::Row), background_color: StyleProp::Value(theme.get().bg), @@ -95,9 +48,6 @@ pub fn TabBar(context: &mut KayakContext, tabs: Vec<String>, selected: usize, on rsx! { <Background styles={Some(background_styles)}> <VecTracker data={tabs} /> - <Element focusable={Some(true)} on_event={Some(add_btn_event_handler)} styles={Some(add_btn_styles)}> - <Text content={"+".to_string()} size={16.0} styles={Some(add_btn_text_styles)} /> - </Element> </Background> } } \ No newline at end of file diff --git a/examples/tabs/tab_box.rs b/examples/tabs/tab_box.rs index 0f7353f489d759711f8b8ef7ae96990f483ca429..1931c0aaf6cb3a5a9871a53dae8a31b60e7d3a27 100644 --- a/examples/tabs/tab_box.rs +++ b/examples/tabs/tab_box.rs @@ -13,12 +13,17 @@ use crate::tab_content::TabContent; #[derive(Debug, Default, Clone, PartialEq)] pub struct TabData { + /// The name of this tab pub name: String, + /// The content to display for this tab, wrapped in a [Fragment] pub content: Fragment, } +/// The actual tab container widget. +/// +/// This houses both the tab bar and its content. #[widget] -pub fn TabBox(context: &mut KayakContext, tabs: Vec<TabData>, initial_tab: usize, on_add_tab: Handler, on_remove_tab: Handler<usize>) { +pub fn TabBox(context: &mut KayakContext, tabs: Vec<TabData>, initial_tab: usize) { let theme = context.create_consumer::<TabTheme>().unwrap_or_default(); let (selected, set_selected, ..) = use_state!(initial_tab); @@ -41,7 +46,7 @@ pub fn TabBox(context: &mut KayakContext, tabs: Vec<TabData>, initial_tab: usize rsx! { <> - <TabBar tabs={tab_names} selected={selected} on_select_tab={on_select_tab} on_add_tab={on_add_tab} on_remove_tab={on_remove_tab} /> + <TabBar tabs={tab_names} selected={selected} on_select_tab={on_select_tab} /> <TabContent tabs={tab_content} selected={selected} /> </> } diff --git a/examples/tabs/tab_content.rs b/examples/tabs/tab_content.rs index 1b5b0245c582921e6df4a7b8e941cd0600255d9b..0dbde71667cb754ed310b928d80061cb1fbf6e91 100644 --- a/examples/tabs/tab_content.rs +++ b/examples/tabs/tab_content.rs @@ -9,11 +9,13 @@ use kayak_ui::{ use crate::TabTheme; +/// A widget that displays the selected tab's content #[widget] pub fn TabContent(context: &mut KayakContext, tabs: Vec<Fragment>, selected: usize) { let theme = context.create_consumer::<TabTheme>().unwrap_or_default(); if selected >= tabs.len() { + // Invalid tab -> don't do anything return; } diff --git a/examples/tabs/tabs.rs b/examples/tabs/tabs.rs index 926ab29ca55393037b392ea3620252355c0638cf..1a169e1ba4f53b4d4da77f04f839ab41f4d052c4 100644 --- a/examples/tabs/tabs.rs +++ b/examples/tabs/tabs.rs @@ -1,3 +1,9 @@ +//! This example demonstrates how one might create a tab system +//! +//! Additionally, it showcases focus navigation. Press `Tab` and `Shift + Tab` to move +//! between focusable widgets. This example also sets it up so that `Enter` or `Space` +//! can be used in place of a normal click. + use bevy::{ prelude::{App as BevyApp, AssetServer, Commands, Res, ResMut}, window::WindowDescriptor, @@ -8,7 +14,7 @@ use kayak_ui::{ bevy::{BevyContext, BevyKayakUIPlugin, FontMapping, UICameraBundle}, core::{ styles::{Style, StyleProp, Units}, - Children, Color, constructor, Handler, Index, render, rsx, use_state, widget + Children, Color, constructor, Index, render, rsx, widget }, widgets::{App, Text, Window}, }; @@ -34,11 +40,14 @@ fn TabDemo() { ..Default::default() }; - let (count, set_count, ..) = use_state!(0); - let (tabs, set_tabs, ..) = use_state!(vec![ + // This is not the most ideal way to generate tabs. For one, the `content` has no access to its actual context + // (i.e. where it actually exists in the hierarchy). Additionally, it would be better if tabs were created as + // children of `TabBox`. These are issues that will be addressed in the future, so for now, this will work. + let tabs = vec![ TabData { name: "Tab 1".to_string(), content: { + // This is a temporary hack to prevent the `constructor` macro from throwing an error let children = Children::None; let text_style = text_style.clone(); constructor! { @@ -72,36 +81,10 @@ fn TabDemo() { } }, }, - ]); - - let tab_clone = tabs.clone(); - let set_added_tabs = set_tabs.clone(); - let on_add_tab = Handler::new(move |_| { - let mut tab_clone = (&tab_clone).clone(); - tab_clone.push(TabData { - name: format!("Tab {}", count), - content: { - let children = Children::None; - constructor! { - <> - <Text content={"Hello".to_string()} size={12.0} /> - </> - } - }, - }, ); - set_count(count + 1); - set_added_tabs(tab_clone); - }); - - let tab_clone = tabs.clone(); - let on_remove_tab = Handler::new(move |index: usize| { - let mut tab_clone = (&tab_clone).clone(); - tab_clone.remove(index); - set_tabs(tab_clone); - }); + ]; rsx! { - <TabBox tabs={tabs} on_add_tab={on_add_tab} on_remove_tab={on_remove_tab} /> + <TabBox tabs={tabs} /> } } @@ -123,19 +106,16 @@ fn startup( normal: Color::new(0.949, 0.956, 0.968, 1.0), hovered: Color::new(0.650, 0.574, 0.669, 1.0), active: Color::new(0.949, 0.956, 0.968, 1.0), - disabled: Color::new(0.662, 0.678, 0.694, 1.0), }, active_tab: ColorState { normal: Color::new(0.286, 0.353, 0.392, 1.0), hovered: Color::new(0.246, 0.323, 0.352, 1.0), - active: Default::default(), - disabled: Color::new(0.474, 0.486, 0.505, 1.0), + active: Color::new(0.196, 0.283, 0.312, 1.0), }, inactive_tab: ColorState { normal: Color::new(0.176, 0.227, 0.255, 1.0), hovered: Color::new(0.16, 0.21, 0.23, 1.0), - active: Default::default(), - disabled: Color::new(0.474, 0.486, 0.505, 1.0), + active: Color::new(0.196, 0.283, 0.312, 1.0), }, tab_height: 22.0, }; diff --git a/examples/tabs/theming.rs b/examples/tabs/theming.rs index cbcc1b3213784931f6ea554698fe2736d8033c41..118908b1c6287e8316d4bc8b36e497ee2ff937f8 100644 --- a/examples/tabs/theming.rs +++ b/examples/tabs/theming.rs @@ -17,7 +17,6 @@ pub struct ColorState { pub normal: Color, pub hovered: Color, pub active: Color, - pub disabled: Color, } #[widget]