WIP: spiked tabs implementation for widgetry demo

TODO:
- tooltips (or is that just a responsibility of the button builder?)
- widgetry color scheme should use day-theme
- pick nice tab colors
- start applying!
  - jump in time
  - info panel
  - "more data"
  - fix width to match widest? Or maybe width modes?
This commit is contained in:
Michael Kirk 2021-03-17 22:13:15 -07:00 committed by Dustin Carlino
parent bf52a84956
commit ab787a5cd1
5 changed files with 241 additions and 84 deletions

View File

@ -63,6 +63,7 @@ pub use crate::widgets::scatter_plot::ScatterPlot;
pub use crate::widgets::slider::Slider;
pub use crate::widgets::spinner::Spinner;
pub use crate::widgets::table;
pub use crate::widgets::tabs::TabController;
pub(crate) use crate::widgets::text_box::TextBox;
pub use crate::widgets::toggle::Toggle;
pub use crate::widgets::{

View File

@ -33,6 +33,7 @@ pub mod scatter_plot;
pub mod slider;
pub mod spinner;
pub mod table;
pub mod tabs;
pub mod text_box;
pub mod toggle;

View File

@ -437,6 +437,11 @@ impl Panel {
}
}
pub fn swap_container_content(&mut self, name: &str, widget: &mut Widget) {
let old_container: &mut Container = self.find_mut(name);
std::mem::swap(&mut old_container.members[0], widget);
}
pub fn rect_of(&self, name: &str) -> &ScreenRectangle {
&self.top_level.find(name).unwrap().rect
}

View File

@ -0,0 +1,128 @@
use crate::{ButtonBuilder, EventCtx, Panel, Widget};
struct Tab {
tab_id: String,
bar_item: ButtonBuilder<'static, 'static>,
content: Widget,
}
impl Tab {
fn new(tab_id: String, bar_item: ButtonBuilder<'static, 'static>, content: Widget) -> Self {
Self {
tab_id,
bar_item,
content,
}
}
fn build_bar_item_widget(&self, ctx: &EventCtx, active: bool) -> Widget {
self.bar_item
.clone()
.disabled(active)
.build_widget(ctx, &self.tab_id)
}
}
pub struct TabController {
id: String,
tabs: Vec<Tab>,
active_child: usize,
}
impl TabController {
pub fn new(
id: String,
initial_bar_item: ButtonBuilder<'static, 'static>,
initial_content: Widget,
) -> Self {
let mut tc = Self {
id,
tabs: vec![],
active_child: 0,
};
tc.push_tab(initial_bar_item, initial_content);
tc
}
/// Add a new tab.
///
/// `bar_item`: The button shown in the tab bar
/// `content`: The content shown when this tab's `bar_item` is clicked
pub fn push_tab(&mut self, bar_item: ButtonBuilder<'static, 'static>, content: Widget) {
let tab_id = self.tab_id(self.tabs.len() + 1);
let tab = Tab::new(tab_id, bar_item, content);
self.tabs.push(tab);
}
/// A widget containing the tab bar and a content pane with the currently active tab.
pub fn build_widget(&mut self, ctx: &EventCtx) -> Widget {
Widget::col(vec![
self.build_bar_items(ctx),
self.pop_active_content()
.container()
.padding(16)
.bg(ctx.style().section_bg)
.named(self.active_content_id()),
])
}
pub fn handle_action(&mut self, ctx: &EventCtx, action: &str, panel: &mut Panel) -> bool {
if !action.starts_with(&self.id) {
return false;
}
let tab_idx = self
.tabs
.iter()
.enumerate()
.find(|(_idx, tab)| &tab.tab_id == action)
.expect(&format!("invalid tab id: {}", action))
.0;
self.activate_tab(ctx, tab_idx, panel);
true
}
fn active_content_id(&self) -> String {
format!("{}_active_content", self.id)
}
fn bar_items_id(&self) -> String {
format!("{}_bar_items", self.id)
}
fn tab_id(&self, tab_index: usize) -> String {
format!("{}_tab_{}", self.id, tab_index)
}
fn pop_active_content(&mut self) -> Widget {
let mut tmp = Widget::nothing();
std::mem::swap(&mut self.tabs[self.active_child].content, &mut tmp);
tmp
}
fn build_bar_items(&self, ctx: &EventCtx) -> Widget {
let bar_items = self
.tabs
.iter()
.enumerate()
.map(|(idx, tab)| tab.build_bar_item_widget(ctx, idx == self.active_child))
.collect();
Widget::row(bar_items)
.container()
.bg(ctx.style().section_bg)
.named(self.bar_items_id())
}
fn activate_tab(&mut self, ctx: &EventCtx, tab_idx: usize, panel: &mut Panel) {
let old_idx = self.active_child;
self.active_child = tab_idx;
let mut bar_items = self.build_bar_items(ctx);
panel.swap_container_content(&self.bar_items_id(), &mut bar_items);
let mut content = self.pop_active_content();
panel.swap_container_content(&self.active_content_id(), &mut content);
self.tabs[old_idx].content = content;
}
}

View File

@ -7,8 +7,8 @@ use geom::{Angle, Duration, Percent, Polygon, Pt2D, Time};
use widgetry::{
lctrl, Choice, Color, ContentMode, Drawable, EventCtx, Fill, GeomBatch, GfxCtx,
HorizontalAlignment, Image, Key, Line, LinePlot, Outcome, Panel, PersistentSplit, PlotOptions,
ScreenDims, Series, SharedAppState, State, Text, TextExt, Texture, Toggle, Transition,
UpdateType, VerticalAlignment, Widget,
ScreenDims, Series, SharedAppState, State, TabController, Text, TextExt, Texture, Toggle,
Transition, UpdateType, VerticalAlignment, Widget,
};
pub fn main() {
@ -36,16 +36,19 @@ struct Demo {
scrollable_canvas: Drawable,
texture_demo: Drawable,
elapsed: Duration,
tabs: TabController,
}
impl Demo {
fn new(ctx: &mut EventCtx) -> Demo {
Demo {
controls: make_controls(ctx),
fn new(ctx: &mut EventCtx) -> Self {
let mut tabs = make_tabs(ctx);
Self {
controls: make_controls(ctx, &mut tabs),
timeseries_panel: None,
scrollable_canvas: setup_scrollable_canvas(ctx),
texture_demo: setup_texture_demo(ctx, Texture::SAND, Texture::CACTUS),
elapsed: Duration::ZERO,
tabs: tabs,
}
}
@ -163,7 +166,9 @@ impl State<App> for Demo {
self.texture_demo = setup_texture_demo(ctx, bg_texture, fg_texture);
}
action => {
if action.contains("btn_") {
if self.tabs.handle_action(ctx, action, &mut self.controls) {
// If true, tab already handled the action internally
} else if action.contains("btn_") {
log::info!("clicked button: {:?}", action);
} else {
unimplemented!("clicked: {:?}", x);
@ -302,14 +307,11 @@ fn setup_scrollable_canvas(ctx: &mut EventCtx) -> Drawable {
batch.upload(ctx)
}
fn make_controls(ctx: &mut EventCtx) -> Panel {
fn make_tabs(ctx: &mut EventCtx) -> TabController {
let style = ctx.style();
Panel::new(Widget::col(vec![
Text::from_multiline(vec![
Line("widgetry demo").big_heading_styled(),
Line("Click and drag the background to pan, use touchpad or scroll wheel to zoom"),
])
.into_widget(ctx),
let gallery_bar_item = style.btn_tab.text("Component Gallery");
let gallery_content = Widget::col(vec![
Text::from(Line("Text").big_heading_styled().size(18)).into_widget(ctx),
Text::from_all(vec![
Line("You can "),
@ -320,7 +322,6 @@ fn make_controls(ctx: &mut EventCtx) -> Panel {
.bg(Color::PURPLE)
.into_widget(ctx),
// Button Style Gallery
// TODO might be nice to have this in separate tabs or something.
Text::from(Line("Buttons").big_heading_styled().size(18)).into_widget(ctx),
Widget::row(vec![
style
@ -411,6 +412,96 @@ fn make_controls(ctx: &mut EventCtx) -> Panel {
]),
Text::from(Line("Spinner").big_heading_styled().size(18)).into_widget(ctx),
widgetry::Spinner::widget(ctx, (0, 11), 1),
]);
let mut tabs = TabController::new("demo_tabs".to_string(), gallery_bar_item, gallery_content);
let qa_bar_item = style.btn_tab.text("Conformance Checks");
let qa_content = Widget::col(vec![
Text::from(
Line("Controls should be same height")
.big_heading_styled()
.size(18),
)
.into_widget(ctx),
{
let row_height = 10;
let mut id = 0;
let mut next_id = || {
id += 1;
format!("btn_height_check_{}", id)
};
Widget::row(vec![
Widget::col(
(0..row_height)
.map(|_| {
style
.btn_outline
.icon("system/assets/tools/layers.svg")
.build_widget(ctx, &next_id())
})
.collect::<Vec<_>>(),
),
Widget::col(
(0..row_height)
.map(|_| style.btn_outline.text("text").build_widget(ctx, &next_id()))
.collect::<Vec<_>>(),
),
Widget::col(
(0..row_height)
.map(|_| {
style
.btn_outline
.icon_text("system/assets/tools/layers.svg", "icon+text")
.build_widget(ctx, &next_id())
})
.collect::<Vec<_>>(),
),
Widget::col(
(0..row_height)
.map(|_| {
style
.btn_popup_icon_text("system/assets/tools/layers.svg", "icon+text")
.build_widget(ctx, &next_id())
})
.collect::<Vec<_>>(),
),
Widget::col(
(0..row_height)
.map(|_| {
style
.btn_outline
.popup("popup")
.build_widget(ctx, &next_id())
})
.collect::<Vec<_>>(),
),
Widget::col(
(0..row_height)
.map(|_| widgetry::Spinner::widget(ctx, (0, 11), 1))
.collect::<Vec<_>>(),
),
Widget::col(
(0..row_height)
.map(|_| widgetry::Toggle::checkbox(ctx, "checkbox", None, true))
.collect::<Vec<_>>(),
),
])
},
]);
tabs.push_tab(qa_bar_item, qa_content);
tabs
}
fn make_controls(ctx: &mut EventCtx, tabs: &mut TabController) -> Panel {
Panel::new(Widget::col(vec![
Text::from_multiline(vec![
Line("widgetry demo").big_heading_styled(),
Line("Click and drag the background to pan, use touchpad or scroll wheel to zoom"),
])
.into_widget(ctx),
Widget::row(vec![
ctx.style()
.btn_outline
@ -495,76 +586,7 @@ fn make_controls(ctx: &mut EventCtx) -> Panel {
.build_widget(ctx, "apply"),
])
.margin_above(30),
Text::from(
Line("Controls should be same height")
.big_heading_styled()
.size(18),
)
.into_widget(ctx),
{
let row_height = 10;
let mut id = 0;
let mut next_id = || {
id += 1;
format!("btn_height_check_{}", id)
};
Widget::row(vec![
Widget::col(
(0..row_height)
.map(|_| {
style
.btn_outline
.icon("system/assets/tools/layers.svg")
.build_widget(ctx, &next_id())
})
.collect::<Vec<_>>(),
),
Widget::col(
(0..row_height)
.map(|_| style.btn_outline.text("text").build_widget(ctx, &next_id()))
.collect::<Vec<_>>(),
),
Widget::col(
(0..row_height)
.map(|_| {
style
.btn_outline
.icon_text("system/assets/tools/layers.svg", "icon+text")
.build_widget(ctx, &next_id())
})
.collect::<Vec<_>>(),
),
Widget::col(
(0..row_height)
.map(|_| {
style
.btn_popup_icon_text("system/assets/tools/layers.svg", "icon+text")
.build_widget(ctx, &next_id())
})
.collect::<Vec<_>>(),
),
Widget::col(
(0..row_height)
.map(|_| {
style
.btn_outline
.popup("popup")
.build_widget(ctx, &next_id())
})
.collect::<Vec<_>>(),
),
Widget::col(
(0..row_height)
.map(|_| widgetry::Spinner::widget(ctx, (0, 11), 1))
.collect::<Vec<_>>(),
),
Widget::col(
(0..row_height)
.map(|_| widgetry::Toggle::checkbox(ctx, "checkbox", None, true))
.collect::<Vec<_>>(),
),
])
},
tabs.build_widget(ctx),
])) // end panel
.aligned(HorizontalAlignment::Center, VerticalAlignment::Top)
.build(ctx)