preserve plot settings over time. also dropdowns

This commit is contained in:
Dustin Carlino 2020-04-06 14:05:59 -07:00
parent 5868fe4736
commit ebf31c33e6
8 changed files with 111 additions and 41 deletions

View File

@ -77,8 +77,9 @@ impl App {
Widget::col(col2).outline(3.0, Color::BLACK).margin(5),
Widget::col(col3).outline(3.0, Color::BLACK).margin(5),
]),
Plot::new_usize(
Plot::new(
ctx,
"timeseries",
vec![
Series {
label: "Linear".to_string(),
@ -114,11 +115,10 @@ impl App {
.aligned(HorizontalAlignment::Percent(0.6), VerticalAlignment::Center)
.build(ctx);
// Since we're creating an entirely new panel when the time changes, it's nice to keep the
// scroll the same. If the new panel got shorter and the old scroll was too long, this
// clamps reasonably.
// Since we're creating an entirely new panel when the time changes, we need to preserve
// some internal state, like scroll and whether plot checkboxes were enabled.
if let Some((_, ref old)) = self.timeseries_panel {
c.restore_scroll(ctx, old.preserve_scroll());
c.restore(ctx, old);
}
c
}
@ -269,8 +269,8 @@ fn make_controls(ctx: &mut EventCtx) -> Composite {
Btn::text_fg("Reset")
.build(ctx, "reset the stopwatch", None)
.margin(5),
Checkbox::new(ctx, "Draw scrollable canvas", None, true).margin(5),
Checkbox::new(ctx, "Show timeseries", lctrl(Key::T), false).margin(5),
Checkbox::text(ctx, "Draw scrollable canvas", None, true).margin(5),
Checkbox::text(ctx, "Show timeseries", lctrl(Key::T), false).margin(5),
])
.evenly_spaced(),
"Stopwatch: ...".draw_text(ctx).named("stopwatch"),

View File

@ -379,6 +379,23 @@ impl Widget {
}
}
fn restore(&mut self, ctx: &mut EventCtx, prev: &Composite) {
if let Some(container) = self.widget.downcast_mut::<Container>() {
for w in &mut container.members {
w.restore(ctx, prev);
}
} else if self.widget.can_restore() {
self.widget.restore(
ctx,
&prev
.top_level
.find(self.id.as_ref().unwrap())
.unwrap()
.widget,
);
}
}
pub fn is_btn(&self, name: &str) -> bool {
self.widget
.downcast_ref::<Button>()
@ -658,12 +675,10 @@ impl Composite {
actions
}
pub fn preserve_scroll(&self) -> (f64, f64) {
self.scroll_offset()
}
pub fn restore(&mut self, ctx: &mut EventCtx, prev: &Composite) {
self.set_scroll_offset(ctx, prev.scroll_offset());
pub fn restore_scroll(&mut self, ctx: &mut EventCtx, offset: (f64, f64)) {
self.set_scroll_offset(ctx, offset);
self.top_level.restore(ctx, &prev);
// Since we just moved things around, let all widgets respond to the mouse being somewhere
ctx.no_op_event(true, |ctx| assert!(self.event(ctx).is_none()));

View File

@ -12,7 +12,7 @@ pub struct Checkbox {
}
impl Checkbox {
// TODO Not typesafe! Gotta pass a button.
// TODO Not typesafe! Gotta pass a button. Also, make sure to give an ID.
pub fn new(enabled: bool, false_btn: Widget, true_btn: Widget) -> Widget {
if enabled {
Widget::new(Box::new(Checkbox {
@ -72,4 +72,17 @@ impl WidgetImpl for Checkbox {
fn draw(&self, g: &mut GfxCtx) {
self.btn.draw(g);
}
fn can_restore(&self) -> bool {
// TODO I'm nervous about doing this one in general, so just do it for plot checkboxes.
self.cb_to_plot.is_some()
}
fn restore(&mut self, _: &mut EventCtx, prev: &Box<dyn WidgetImpl>) {
let prev = prev.downcast_ref::<Checkbox>().unwrap();
if self.enabled != prev.enabled {
std::mem::swap(&mut self.btn, &mut self.other_btn);
self.btn.set_pos(self.other_btn.top_left);
self.enabled = !self.enabled;
}
}
}

View File

@ -47,6 +47,33 @@ impl<T: 'static + PartialEq + Clone> Dropdown<T> {
}
}
impl<T: 'static + Clone> Dropdown<T> {
fn open_menu(&mut self, ctx: &mut EventCtx) {
// TODO set current idx in menu
let mut menu = Menu::new(
ctx,
self.choices
.iter()
.enumerate()
.map(|(idx, c)| c.with_value(idx))
.collect(),
)
.take_menu();
let y1_below = self.btn.top_left.y + self.btn.dims.height + 15.0;
menu.set_pos(ScreenPt::new(
self.btn.top_left.x,
// top_left_for_corner doesn't quite work
if y1_below + menu.get_dims().height < ctx.canvas.window_height {
y1_below
} else {
self.btn.top_left.y - 15.0 - menu.get_dims().height
},
));
self.menu = Some(menu);
}
}
impl<T: 'static + Clone> WidgetImpl for Dropdown<T> {
fn get_dims(&self) -> ScreenDims {
self.btn.get_dims()
@ -81,28 +108,7 @@ impl<T: 'static + Clone> WidgetImpl for Dropdown<T> {
} else {
self.btn.event(ctx, output);
if output.outcome.take().is_some() {
// TODO set current idx in menu
let mut menu = Menu::new(
ctx,
self.choices
.iter()
.enumerate()
.map(|(idx, c)| c.with_value(idx))
.collect(),
)
.take_menu();
let y1_below = self.btn.top_left.y + self.btn.dims.height + 15.0;
menu.set_pos(ScreenPt::new(
self.btn.top_left.x,
// top_left_for_corner doesn't quite work
if y1_below + menu.get_dims().height < ctx.canvas.window_height {
y1_below
} else {
self.btn.top_left.y - 15.0 - menu.get_dims().height
},
));
self.menu = Some(menu);
self.open_menu(ctx);
}
}
}
@ -137,6 +143,18 @@ impl<T: 'static + Clone> WidgetImpl for Dropdown<T> {
.mark_covered_area(ScreenRectangle::top_left(m.top_left, m.get_dims()));
}
}
fn can_restore(&self) -> bool {
true
}
fn restore(&mut self, ctx: &mut EventCtx, prev: &Box<dyn WidgetImpl>) {
let prev = prev.downcast_ref::<Dropdown<T>>().unwrap();
if prev.menu.is_some() {
self.open_menu(ctx);
// TODO Preserve menu hovered item. Only matters if we've moved the cursor off the
// menu.
}
}
}
fn make_btn(ctx: &EventCtx, name: &str, label: &str, blank_btn_label: bool) -> Button {

View File

@ -28,6 +28,16 @@ pub trait WidgetImpl: downcast_rs::Downcast {
fn event(&mut self, ctx: &mut EventCtx, output: &mut WidgetOutput);
/// Draw the widget. Be sure to draw relative to the top-left specified by `set_pos`.
fn draw(&self, g: &mut GfxCtx);
/// If a new Composite is being created to replace an older one, all widgets have the chance to
/// preserve state from the previous version.
fn can_restore(&self) -> bool {
false
}
/// Restore state from the previous version of this widget, with the same ID. Implementors must
/// downcast.
fn restore(&mut self, _ctx: &mut EventCtx, _prev: &Box<dyn WidgetImpl>) {
unreachable!()
}
/// Internal hack. Don't override.
fn update_series(&mut self, _label: String, _enabled: bool) {

View File

@ -64,6 +64,10 @@ impl<T: Yvalue<T>> Plot<T> {
.take_checkbox()
.callback_to_plot(id, &s.label),
))
// TODO Messy! We have to remember to repeat what Checkbox::text does,
// because we used take_checkbox
.named(&s.label)
.outline(ctx.style().outline_thickness, ctx.style().outline_color)
})
.collect(),
)
@ -294,6 +298,16 @@ impl<T: Yvalue<T>> WidgetImpl for Plot<T> {
}
panic!("Plot doesn't have a series {}", label);
}
fn can_restore(&self) -> bool {
true
}
fn restore(&mut self, _: &mut EventCtx, prev: &Box<dyn WidgetImpl>) {
let prev = prev.downcast_ref::<Plot<T>>().unwrap();
for (s1, s2) in self.series.iter_mut().zip(prev.series.iter()) {
s1.enabled = s2.enabled;
}
}
}
pub trait Yvalue<T>: 'static + Copy + std::cmp::Ord {

View File

@ -59,9 +59,9 @@ impl TrafficSignalEditor {
fn change_phase(&mut self, idx: usize, app: &App, ctx: &mut EventCtx) {
if self.current_phase == idx {
let preserve_scroll = self.composite.preserve_scroll();
self.composite = make_signal_diagram(ctx, app, self.i, self.current_phase, true);
self.composite.restore_scroll(ctx, preserve_scroll);
let mut new = make_signal_diagram(ctx, app, self.i, self.current_phase, true);
new.restore(ctx, &self.composite);
self.composite = new;
} else {
self.current_phase = idx;
self.composite = make_signal_diagram(ctx, app, self.i, self.current_phase, true);

View File

@ -342,9 +342,9 @@ impl InfoPanel {
// Live update?
if app.primary.sim.time() != self.time || ctx_actions.is_paused() != self.is_paused {
let preserve_scroll = self.composite.preserve_scroll();
*self = InfoPanel::new(ctx, app, self.tab.clone(), ctx_actions);
self.composite.restore_scroll(ctx, preserve_scroll);
let mut new = InfoPanel::new(ctx, app, self.tab.clone(), ctx_actions);
new.composite.restore(ctx, &self.composite);
*self = new;
return (false, None);
}