Skip to content

Commit

Permalink
Remove state from segmented control widget
Browse files Browse the repository at this point in the history
  • Loading branch information
knoellle committed Jan 21, 2025
1 parent 81b53ed commit a1244f3
Show file tree
Hide file tree
Showing 4 changed files with 54 additions and 92 deletions.
73 changes: 22 additions & 51 deletions crates/hulk_widgets/src/segmented_control.rs
Original file line number Diff line number Diff line change
@@ -1,26 +1,20 @@
use egui::{
vec2, Align2, Context, Id, InnerResponse, Key, Rect, Response, Rounding, Sense, TextStyle, Ui,
Widget,
};
use egui::{vec2, Align2, Id, Key, Rect, Response, Rounding, Sense, TextStyle, Ui, Widget};

const ANIMATION_TIME_SECONDS: f32 = 0.1;

pub struct SegmentedControl<'ui, T> {
selectables: &'ui [T],
id: Id,
selected: &'ui mut usize,
selectables: &'ui [T],
rounding: Option<Rounding>,
text_style: TextStyle,
}

#[derive(Debug, Default, Clone)]
struct SegmentedControlState {
selected: usize,
}

impl<'ui, T: ToString> SegmentedControl<'ui, T> {
pub fn new(id: impl Into<Id>, selectables: &'ui [T]) -> Self {
pub fn new(id: impl Into<Id>, selected: &'ui mut usize, selectables: &'ui [T]) -> Self {
SegmentedControl {
id: id.into(),
selected,
selectables,
rounding: None,
text_style: TextStyle::Body,
Expand All @@ -31,25 +25,20 @@ impl<'ui, T: ToString> SegmentedControl<'ui, T> {
self.rounding = Some(rounding.into());
self
}
}

pub fn ui(self, ui: &mut Ui) -> InnerResponse<&'ui T> {
let mut state = load_state(ui.ctx(), self.id);
let response = self.show(ui, &mut state);
let selected = &self.selectables[state.selected];
save_state(ui.ctx(), self.id, state);
InnerResponse::new(selected, response)
}

fn show(&self, ui: &mut Ui, state: &mut SegmentedControlState) -> Response {
impl<T: ToString> Widget for SegmentedControl<'_, T> {
fn ui(mut self, ui: &mut Ui) -> Response {
let this = &mut self;
let width = ui.available_width();
let text_style = ui
.style()
.text_styles
.get(&self.text_style)
.get(&this.text_style)
.expect("failed to get text style")
.clone();
let text_size = text_style.size * ui.ctx().pixels_per_point();
let rounding = self
let rounding = this
.rounding
.unwrap_or(ui.style().noninteractive().rounding);

Expand All @@ -58,41 +47,41 @@ impl<'ui, T: ToString> SegmentedControl<'ui, T> {
if response.contains_pointer() {
ui.input(|reader| {
if reader.key_pressed(Key::ArrowLeft) || reader.key_pressed(Key::ArrowDown) {
state.selected = state.selected.saturating_sub(1);
*this.selected = this.selected.saturating_sub(1);
response.mark_changed();
} else if reader.key_pressed(Key::ArrowRight) || reader.key_pressed(Key::ArrowUp) {
state.selected = (state.selected + 1).min(self.selectables.len() - 1);
*this.selected = (*this.selected + 1).min(this.selectables.len() - 1);
response.mark_changed();
}
})
}
painter.rect_filled(response.rect, rounding, ui.style().visuals.extreme_bg_color);

let text_rects = text_rects(response.rect, self.selectables.len());
let text_rects = text_rects(response.rect, this.selectables.len());
let offset = text_rects[0].width();

let translation = ui.ctx().animate_value_with_time(
self.id,
offset * state.selected as f32,
this.id,
offset * *this.selected as f32,
ANIMATION_TIME_SECONDS,
);
let selector_rect = text_rects[0].translate(vec2(translation, 0.0)).shrink(2.0);
let selector_response =
ui.interact(selector_rect, self.id.with("selector"), Sense::click());
ui.interact(selector_rect, this.id.with("selector"), Sense::click());
let selector_style = ui.style().interact(&selector_response);
painter.rect_filled(selector_rect, rounding, selector_style.bg_fill);

let noninteractive_style = ui.style().noninteractive();

for (idx, (&rect, text)) in text_rects.iter().zip(self.selectables.iter()).enumerate() {
let label_response = ui.interact(rect, self.id.with(idx), Sense::click());
for (idx, (&rect, text)) in text_rects.iter().zip(this.selectables.iter()).enumerate() {
let label_response = ui.interact(rect, this.id.with(idx), Sense::click());
let style = ui.style().interact(&response);

let show_line = idx > 0 && state.selected != idx && state.selected + 1 != idx;
let show_line = idx > 0 && *this.selected != idx && *this.selected + 1 != idx;
{
let animated_height = ui
.ctx()
.animate_bool(self.id.with("vline").with(idx), show_line);
.animate_bool(this.id.with("vline").with(idx), show_line);

let height = vec2(0.0, rect.height() - 4.0);
let center = rect.left_center();
Expand All @@ -107,7 +96,7 @@ impl<'ui, T: ToString> SegmentedControl<'ui, T> {
}

if label_response.clicked() {
state.selected = idx;
*this.selected = idx;
response.mark_changed();
}
painter.text(
Expand All @@ -122,24 +111,6 @@ impl<'ui, T: ToString> SegmentedControl<'ui, T> {
}
}

impl<T: ToString> Widget for SegmentedControl<'_, T> {
fn ui(self, ui: &mut Ui) -> Response {
let mut state = load_state(ui.ctx(), self.id);
let response = self.show(ui, &mut state);
save_state(ui.ctx(), self.id, state);
response
}
}

fn load_state(ctx: &Context, id: Id) -> SegmentedControlState {
let persisted = ctx.data_mut(|reader| reader.get_temp(id));
persisted.unwrap_or_default()
}

fn save_state(ctx: &Context, id: Id, state: SegmentedControlState) {
ctx.data_mut(|writer| writer.insert_temp(id, state));
}

fn text_rects(mut rect: Rect, number_of_texts: usize) -> Vec<Rect> {
let base_width = rect.width() / number_of_texts as f32;
let base_rect = {
Expand Down
11 changes: 7 additions & 4 deletions tools/twix/src/panels/behavior_simulator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,13 @@ impl Widget for &mut BehaviorSimulatorPanel {
ui.add_space(50.0);

let robots = (1..=7).collect::<Vec<_>>();
let robot_selection =
SegmentedControl::new("robot-selector", &robots).ui(ui);
self.selected_robot = *robot_selection.inner;
if robot_selection.response.changed() {
let response = SegmentedControl::new(
"robot-selector",
&mut self.selected_robot,
&robots,
)
.ui(ui);
if response.changed() {
self.nao.write(
"parameters.selected_robot",
TextOrBinary::Text(self.selected_robot.into()),
Expand Down
42 changes: 14 additions & 28 deletions tools/vista/src/app.rs
Original file line number Diff line number Diff line change
@@ -1,41 +1,27 @@
use std::fmt::{Display, Error, Formatter};

use eframe::{
egui::{
pos2, vec2, Align2, CentralPanel, Color32, FontId, Key, Modifiers, ScrollArea, Shape,
TopBottomPanel,
TopBottomPanel, Widget,
},
epaint::{PathStroke, QuadraticBezierShape},
App, CreationContext,
};

use hulk_manifest::collect_hulk_cyclers;
use hulk_widgets::SegmentedControl;
use repository::Repository;
use source_analyzer::{contexts::Field, cyclers::Cyclers};

pub struct DependencyInspector {
_repository: Repository,
cyclers: Cyclers,
selected_cycler: usize,
selected_node_index: Option<usize>,
}

struct NamedIndex<'a> {
index: usize,
name: &'a str,
}

impl Display for NamedIndex<'_> {
fn fmt(&self, formatter: &mut Formatter<'_>) -> Result<(), Error> {
formatter.write_str(self.name)
}
}

impl DependencyInspector {
pub fn new(_creation_context: &CreationContext, repository: Repository) -> Self {
let cyclers = collect_hulk_cyclers(repository.crates_directory()).unwrap();
Self {
_repository: repository,
cyclers,
selected_cycler: 0,
selected_node_index: None,
Expand All @@ -50,16 +36,12 @@ impl App for DependencyInspector {
.cyclers
.cyclers
.iter()
.enumerate()
.map(|(index, cycler)| NamedIndex {
index,
name: &cycler.name,
})
.map(|cycler| &cycler.name)
.collect();
let response =
hulk_widgets::SegmentedControl::new("cycler selector", &cycler_names).ui(ui);
if response.response.changed() {
self.selected_cycler = response.inner.index;
SegmentedControl::new("cycler selector", &mut self.selected_cycler, &cycler_names)
.ui(ui);
if response.changed() {
self.selected_node_index = None;
}
});
Expand All @@ -86,13 +68,17 @@ impl App for DependencyInspector {
let mut node_selection_changed = false;
ui.input_mut(|input| {
if input.consume_key(Modifiers::NONE, Key::ArrowUp) {
self.selected_node_index =
Some(self.selected_node_index.unwrap_or(0).saturating_sub(1));
self.selected_node_index = Some(match self.selected_node_index {
Some(old) => old.saturating_sub(1),
None => nodes.len() - 1,
});
node_selection_changed = true;
}
if input.consume_key(Modifiers::NONE, Key::ArrowDown) {
self.selected_node_index =
Some((self.selected_node_index.unwrap_or(0) + 1).min(nodes.len() - 1));
self.selected_node_index = Some(match self.selected_node_index {
Some(old) => (old + 1).min(nodes.len() - 1),
None => 0,
});
node_selection_changed = true;
}
if input.consume_key(Modifiers::NONE, Key::Escape) {
Expand Down
20 changes: 11 additions & 9 deletions tools/widget_gallery/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use eframe::{
egui::{CentralPanel, Context},
egui::{CentralPanel, Context, Widget},
run_native, App, Frame,
};
use hulk_widgets::{CompletionEdit, SegmentedControl};
Expand All @@ -15,15 +15,17 @@ fn main() -> eframe::Result {
#[derive(Debug, Clone)]
struct AppState {
searchables: Vec<String>,
selected: String,
selected: usize,
search_text: String,
}

impl AppState {
pub fn new() -> Self {
let searchables: Vec<_> = (1..100).map(|x| x.to_string()).collect();
Self {
searchables,
selected: String::new(),
selected: 0,
search_text: String::new(),
}
}
}
Expand All @@ -35,10 +37,10 @@ impl App for AppState {
let response = ui.add(CompletionEdit::new(
"completion-edit",
&self.searchables,
&mut self.selected,
&mut self.search_text,
));
if response.changed() {
println!("Selected: {}", self.selected);
println!("Selected: {}", self.search_text);
}

if ui.button("Focus").clicked() {
Expand All @@ -51,10 +53,10 @@ impl App for AppState {
ui.horizontal(|ui| {
ui.columns(2, |columns| {
let selectables = ["Dies", "Das", "Ananas", "Foo", "Bar", "Baz"];
let selected = SegmentedControl::new("segmented-control", &selectables)
.ui(&mut columns[0])
.inner;
columns[1].label(*selected);
SegmentedControl::new("segmented-control", &mut self.selected, &selectables)
.ui(&mut columns[0]);

columns[1].label(selectables[self.selected]);
})
})
});
Expand Down

0 comments on commit a1244f3

Please sign in to comment.