Skip to content
Snippets Groups Projects
Commit d6263f15 authored by MrGVSV's avatar MrGVSV
Browse files

Added tabs example

parent ed8083df
No related branches found
No related tags found
No related merge requests found
......@@ -29,3 +29,7 @@ bevy = { version = "0.6.0" }
[[example]]
name = "todo"
path = "examples/todo/todo.rs"
[[example]]
name = "tabs"
path = "examples/tabs/tabs.rs"
use kayak_ui::{
core::{
render_command::RenderCommand,
styles::{LayoutType, Style, StyleProp, Units},
Bound, EventType, Handler, KeyCode, OnEvent, rsx, use_state, widget,
},
widgets::{Background, Text},
};
use crate::TabTheme;
#[derive(Clone, PartialEq)]
enum TabHoverState {
None,
Inactive,
Active,
}
#[widget]
pub fn Tab(context: &mut KayakContext, content: String, selected: bool, on_request_remove: Handler) {
let theme = context.create_consumer::<TabTheme>().unwrap_or_default();
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 {
EventType::Hover => {
if selected {
set_hover_state(TabHoverState::Active);
} else {
set_hover_state(TabHoverState::Inactive);
}
}
EventType::MouseOut => {
set_hover_state(TabHoverState::None);
}
EventType::Focus => {
set_focus_state(true);
}
EventType::Blur => {
set_focus_state(false);
}
_ => {}
}
});
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,
TabHoverState::Inactive => theme.get().inactive_tab.hovered,
TabHoverState::Active => theme.get().active_tab.hovered,
};
let pad_x = Units::Pixels(2.0);
let bg_styles = Style {
background_color: StyleProp::Value(tab_color),
layout_type: StyleProp::Value(LayoutType::Row),
padding_left: StyleProp::Value(pad_x),
padding_right: StyleProp::Value(pad_x),
..Default::default()
};
let border_width = Units::Pixels(2.0);
let border_styles = Style {
background_color: if focus_state {
StyleProp::Value(theme.get().focus)
} else {
StyleProp::Value(tab_color)
},
padding_left: StyleProp::Value(border_width),
padding_right: StyleProp::Value(border_width),
padding_top: StyleProp::Value(border_width),
padding_bottom: StyleProp::Value(border_width),
layout_type: StyleProp::Value(LayoutType::Row),
..Default::default()
};
let text_styles = Style {
background_color: if focus_state {
StyleProp::Value(theme.get().focus)
} else {
StyleProp::Value(tab_color)
},
color: StyleProp::Value(theme.get().text.normal),
top: StyleProp::Value(Units::Stretch(0.1)),
bottom: StyleProp::Value(Units::Stretch(1.0)),
width: StyleProp::Value(Units::Stretch(1.0)),
..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)),
max_width: StyleProp::Value(Units::Pixels(100.0)),
..styles.clone().unwrap_or_default()
});
rsx! {
<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>
}
}
\ No newline at end of file
use kayak_ui::{
core::{
styles::{LayoutType, Style, StyleProp, Units},
Bound, constructor, EventType, Handler, KeyCode, OnEvent,
rsx, use_state, VecTracker, widget,
},
widgets::{Background, Element, Text},
};
use crate::tab::Tab;
use crate::TabTheme;
#[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>) {
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();
let tab_event_handler = OnEvent::new(move |_, event| {
match event.event_type {
EventType::Click => {
on_select.call(index);
}
EventType::KeyDown(evt) => {
if evt.key() == KeyCode::Return || evt.key() == KeyCode::Space {
on_select.call(index);
}
}
_ => {}
}
});
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} />
}
}).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),
height: StyleProp::Value(Units::Auto),
width: StyleProp::Value(Units::Stretch(1.0)),
..styles.clone().unwrap_or_default()
};
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
use std::fmt::Debug;
use kayak_ui::{
core::{
render_command::RenderCommand,
styles::{Style, StyleProp},
Bound, Fragment, Handler, rsx, use_state, widget,
}
};
use crate::tab_bar::TabBar;
use crate::TabTheme;
use crate::tab_content::TabContent;
#[derive(Debug, Default, Clone, PartialEq)]
pub struct TabData {
pub name: String,
pub content: Fragment,
}
#[widget]
pub fn TabBox(context: &mut KayakContext, tabs: Vec<TabData>, initial_tab: usize, on_add_tab: Handler, on_remove_tab: Handler<usize>) {
let theme = context.create_consumer::<TabTheme>().unwrap_or_default();
let (selected, set_selected, ..) = use_state!(initial_tab);
let tab_names = tabs.iter().map(|tab| {
tab.name.clone()
}).collect::<Vec<String>>();
let tab_content = tabs.iter().map(|tab| {
tab.content.clone()
}).collect::<Vec<_>>();
let on_select_tab = Handler::<usize>::new(move |index| {
set_selected(index);
});
self.styles = Some(Style {
render_command: StyleProp::Value(RenderCommand::Quad),
background_color: StyleProp::Value(theme.get().fg),
..Default::default()
});
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} />
<TabContent tabs={tab_content} selected={selected} />
</>
}
}
\ No newline at end of file
use std::ops::Index;
use kayak_ui::{
core::{
render_command::RenderCommand,
styles::{Style, StyleProp},
Bound, Fragment, rsx, VecTracker, widget,
}
};
use crate::TabTheme;
#[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() {
return;
}
self.styles = Some(Style {
render_command: StyleProp::Value(RenderCommand::Quad),
background_color: StyleProp::Value(theme.get().fg),
..Default::default()
});
let tab = tabs.index(selected).clone();
rsx! {
<>
<VecTracker data={vec![tab.clone()]} />
</>
}
}
\ No newline at end of file
use bevy::{
prelude::{App as BevyApp, AssetServer, Commands, Res, ResMut},
window::WindowDescriptor,
DefaultPlugins,
};
use kayak_ui::{
bevy::{BevyContext, BevyKayakUIPlugin, FontMapping, UICameraBundle},
core::{
styles::{Style, StyleProp, Units},
Children, Color, constructor, Handler, Index, render, rsx, use_state, widget
},
widgets::{App, Text, Window},
};
use tab_box::TabBox;
use tab_box::TabData;
use crate::theming::{ColorState, TabTheme, TabThemeProvider};
mod tab_bar;
mod tab_box;
mod tab_content;
mod tab;
mod theming;
#[widget]
fn TabDemo() {
let text_style = Style {
width: StyleProp::Value(Units::Percentage(75.0)),
top: StyleProp::Value(Units::Stretch(0.5)),
left: StyleProp::Value(Units::Stretch(1.0)),
bottom: StyleProp::Value(Units::Stretch(1.0)),
right: StyleProp::Value(Units::Stretch(1.0)),
..Default::default()
};
let (count, set_count, ..) = use_state!(0);
let (tabs, set_tabs, ..) = use_state!(vec![
TabData {
name: "Tab 1".to_string(),
content: {
let children = Children::None;
let text_style = text_style.clone();
constructor! {
<>
<Text content={"Welcome to Tab 1!".to_string()} size={48.0} styles={Some(text_style)} />
</>
}
},
},
TabData {
name: "Tab 2".to_string(),
content: {
let children = Children::None;
let text_style = text_style.clone();
constructor! {
<>
<Text content={"Welcome to Tab 2!".to_string()} size={48.0} styles={Some(text_style)} />
</>
}
},
},
TabData {
name: "Tab 3".to_string(),
content: {
let children = Children::None;
let text_style = text_style.clone();
constructor! {
<>
<Text content={"Welcome to Tab 3!".to_string()} size={48.0} styles={Some(text_style)} />
</>
}
},
},
]);
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} />
}
}
fn startup(
mut commands: Commands,
mut font_mapping: ResMut<FontMapping>,
asset_server: Res<AssetServer>,
) {
commands.spawn_bundle(UICameraBundle::new());
font_mapping.add(asset_server.load("roboto.kayak_font"));
let theme = TabTheme {
primary: Default::default(),
bg: Color::new(0.176, 0.227, 0.255, 1.0),
fg: Color::new(0.286, 0.353, 0.392, 1.0),
focus: Color::new(0.388, 0.474, 0.678, 0.5),
text: ColorState {
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),
},
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),
},
tab_height: 22.0,
};
let context = BevyContext::new(|context| {
render! {
<App>
<Window position={(50.0, 50.0)} size={(600.0, 300.0)} title={"Tabs Example".to_string()}>
<TabThemeProvider initial_theme={theme}>
<TabDemo />
</TabThemeProvider>
</Window>
</App>
}
});
commands.insert_resource(context);
}
fn main() {
BevyApp::new()
.insert_resource(WindowDescriptor {
width: 1270.0,
height: 720.0,
title: String::from("UI Example"),
..Default::default()
})
.add_plugins(DefaultPlugins)
.add_plugin(BevyKayakUIPlugin)
.add_startup_system(startup)
.run();
}
\ No newline at end of file
use kayak_ui::{core::{Color, rsx, widget}};
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct TabTheme {
pub primary: Color,
pub bg: Color,
pub fg: Color,
pub focus: Color,
pub text: ColorState,
pub active_tab: ColorState,
pub inactive_tab: ColorState,
pub tab_height: f32
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct ColorState {
pub normal: Color,
pub hovered: Color,
pub active: Color,
pub disabled: Color,
}
#[widget]
pub fn TabThemeProvider(initial_theme: TabTheme) {
context.create_provider(initial_theme);
rsx! { <>{children}</> }
}
\ No newline at end of file
......@@ -94,12 +94,6 @@ impl EventDispatcher {
// --- Call Event --- //
let mut target_widget = context.widget_manager.take(index);
target_widget.on_event(context, &mut node_event);
// if target_widget.get_name() == String::from("TextBox") {
// println!("Event: {:#?}", node_event);
// println!("Widget Focus: {} - {:?}", target_widget.focusable(), index);
// println!("Context Focus: {} - {:?}", context.get_focusable(index), index);
// }
// target_widget.set_focusable(false);
context.widget_manager.repossess(target_widget);
event.default_prevented |= node_event.default_prevented;
......
......@@ -36,14 +36,7 @@ impl FocusTree {
}
}
if let Some(root) = self.tree.root_node {
// Replace root node
self.tree.replace(root, index);
if widget_tree.is_descendant(root, index) {
// If old root is child -> add it back in
self.add(root, &widget_tree);
}
} else {
if self.tree.root_node.is_none() {
// Set root node
self.tree.add(index, None);
self.focus(index);
......@@ -158,6 +151,10 @@ impl FocusTree {
self.tree.root_node
}
pub fn tree(&self) -> &Tree {
&self.tree
}
}
......
......@@ -63,7 +63,7 @@ impl OnEvent {
}
#[derive(Clone)]
pub struct Handler<T>(pub Arc<RwLock<dyn FnMut(T) + Send + Sync + 'static>>);
pub struct Handler<T = ()>(pub Arc<RwLock<dyn FnMut(T) + Send + Sync + 'static>>);
impl<T> Default for Handler<T> {
fn default() -> Self {
......
......@@ -360,7 +360,7 @@ impl WidgetManager {
)
}
fn build_nodes_tree(&self) -> Tree {
fn build_nodes_tree(&mut self) -> Tree {
let mut tree = Tree::default();
let (root_node_id, _) = self.current_widgets.iter().next().unwrap();
tree.root_node = Some(root_node_id);
......@@ -368,6 +368,11 @@ impl WidgetManager {
tree.root_node.unwrap(),
self.get_valid_node_children(tree.root_node.unwrap()),
);
let old_focus = self.focus_tree.current();
self.focus_tree.clear();
self.focus_tree.add(root_node_id, &self.tree);
for (widget_id, widget) in self.current_widgets.iter().skip(1) {
let widget_styles = widget.as_ref().unwrap().get_styles();
if let Some(widget_styles) = widget_styles {
......@@ -381,7 +386,19 @@ impl WidgetManager {
}
}
}
let focusable = self.get_focusable(widget_id).unwrap_or_default();
if focusable {
self.focus_tree.add(widget_id, &self.tree);
}
}
if let Some(old_focus) = old_focus {
if self.focus_tree.contains(old_focus) {
self.focus_tree.focus(old_focus);
}
}
tree
}
......@@ -426,16 +443,6 @@ impl WidgetManager {
}
pub fn set_focusable(&mut self, focusable: Option<bool>, index: Index, is_parent: bool) {
let is_focusable = self.get_focusable(index).unwrap_or_default();
self.focus_tracker.set_focusability(index, focusable, is_parent);
let focusable = self.get_focusable(index).unwrap_or_default();
if focusable {
if !is_focusable {
self.focus_tree.add(index, &self.tree);
}
} else if is_focusable {
self.focus_tree.remove(index);
}
}
}
......@@ -6,7 +6,7 @@ use crate::core::{
widget, Children, Fragment,
};
#[widget]
#[widget(focusable)]
pub fn Button(children: Children, styles: Option<Style>) {
let base_styles = styles.clone().unwrap_or_default();
*styles = Some(Style {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment