Skip to content

Commit

Permalink
polish completion edit
Browse files Browse the repository at this point in the history
  • Loading branch information
oleflb committed Oct 27, 2024
1 parent ea83607 commit 536aaec
Show file tree
Hide file tree
Showing 26 changed files with 418 additions and 573 deletions.
156 changes: 74 additions & 82 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,6 @@ fern = { version = "0.6.1", features = ["colored"] }
filtering = { path = "crates/filtering" }
framework = { path = "crates/framework" }
futures-util = "0.3.24"
fuzzy-matcher = "0.3.7"
geometry = { path = "crates/geometry" }
gilrs = "0.10.1"
glob = "0.3.0"
Expand Down
24 changes: 15 additions & 9 deletions crates/hulk_replayer/src/labels.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::collections::BTreeMap;

use eframe::egui::{
pos2, vec2, Align, Layout, Rect, Response, RichText, Sense, TextStyle, Ui, Widget,
pos2, vec2, Align, Layout, Rect, Response, RichText, Sense, TextStyle, Ui, UiBuilder, Widget,
};

use framework::Timing;
Expand Down Expand Up @@ -43,14 +43,20 @@ impl<'state> Widget for Labels<'state> {
left_top,
pos2(ui.max_rect().right(), left_top.y + row_height),
);
let mut child_ui = ui.child_ui(child_rect, Layout::top_down(Align::Min), None);
child_ui.set_height(row_height);
child_ui.label(RichText::new(label_content.name).strong());
let text_height = ui.style().text_styles.get(&TextStyle::Body).unwrap().size;
if child_ui.available_height() >= text_height {
child_ui.label(format!("{} frames", label_content.number_of_frames));
}
maximum_width = maximum_width.max(child_ui.min_size().x);
ui.scope_builder(
UiBuilder::new()
.max_rect(child_rect)
.layout(Layout::top_down(Align::Min)),
|ui| {
ui.set_height(row_height);
ui.label(RichText::new(label_content.name).strong());
let text_height = ui.style().text_styles.get(&TextStyle::Body).unwrap().size;
if ui.available_height() >= text_height {
ui.label(format!("{} frames", label_content.number_of_frames));
}
maximum_width = maximum_width.max(ui.min_size().x);
},
);
}

ui.allocate_rect(
Expand Down
13 changes: 9 additions & 4 deletions crates/hulk_replayer/src/timeline.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::collections::BTreeMap;

use eframe::egui::{vec2, Align, Layout, Rect, Response, Ui, Vec2, Widget};
use eframe::egui::{vec2, Align, Layout, Rect, Response, Ui, UiBuilder, Vec2, Widget};

use framework::Timing;

Expand Down Expand Up @@ -43,8 +43,6 @@ impl<'state> Widget for Timeline<'state> {
ui.max_rect().left_top(),
vec2(ui.available_width(), ticks_height(ui)),
);
let mut ticks_ui =
ui.child_ui(ticks_rect, Layout::top_down_justified(Align::Min), None);
ui.advance_cursor_after_rect(ticks_rect);

let response = ui.add(Frames::new(
Expand All @@ -55,7 +53,14 @@ impl<'state> Widget for Timeline<'state> {
original_item_spacing,
));

Ticks::new(self.frame_range, self.viewport_range, self.position).ui(&mut ticks_ui);
ui.scope_builder(
UiBuilder::new()
.max_rect(ticks_rect)
.layout(Layout::top_down_justified(Align::Min)),
|ui| {
Ticks::new(self.frame_range, self.viewport_range, self.position).ui(ui);
},
);

response
})
Expand Down
7 changes: 4 additions & 3 deletions crates/hulk_widgets/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ license.workspace = true
homepage.workspace = true

[dependencies]
egui.workspace = true
egui_extras.workspace = true
nucleo-matcher.workspace = true
communication = { workspace = true }
egui = { workspace = true }
egui_extras = { workspace = true }
nucleo-matcher = { workspace = true }
108 changes: 60 additions & 48 deletions crates/hulk_widgets/src/completion_edit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,19 @@ use std::{cmp::Reverse, fmt::Debug};
use egui::{
popup_below_widget,
text::{CCursor, CCursorRange},
text_edit::TextEditOutput,
util::cache::{ComputerMut, FrameCache},
Context, Id, Key, Modifiers, PopupCloseBehavior, Response, ScrollArea, TextEdit, TextStyle, Ui,
Widget,
Context, Id, Key, PopupCloseBehavior, Response, ScrollArea, TextEdit, TextStyle, Ui, Widget,
};
use nucleo_matcher::{
pattern::{CaseMatching, Normalization, Pattern},
Matcher, Utf32Str,
};

pub struct CompletionEdit<'a, 'b, T> {
pub struct CompletionEdit<'a, T> {
id: Id,
items: &'a [T],
selected: &'b mut Option<&'a T>,
suggestions: &'a [T],
selected: &'a mut String,
}

#[derive(Debug, Clone, Copy, Default, PartialEq)]
Expand All @@ -27,9 +27,22 @@ enum UserState {
},
}

impl UserState {
fn handle_arrow(self, pressed_down: bool, pressed_up: bool, number_of_items: usize) -> Self {
match (pressed_up, pressed_down, self) {
(_, true, UserState::Typing) => UserState::Selecting { index: 0 },
(true, _, UserState::Selecting { index: 0 }) => UserState::Typing,
(true, _, UserState::Selecting { index }) => UserState::Selecting { index: index - 1 },
(_, true, UserState::Selecting { index }) => UserState::Selecting {
index: (index + 1).min(number_of_items - 1),
},
(_, _, state) => state,
}
}
}

#[derive(Debug, Clone, Default)]
struct CompletionEditState {
current_search: String,
user_state: UserState,
}

Expand Down Expand Up @@ -79,11 +92,11 @@ impl CompletionEditState {
}
}

impl<'a, 'b, T: ToString + Debug + std::hash::Hash> CompletionEdit<'a, 'b, T> {
pub fn new(id_salt: impl Into<Id>, items: &'a [T], selected: &'b mut Option<&'a T>) -> Self {
impl<'a, T: ToString + Debug + std::hash::Hash> CompletionEdit<'a, T> {
pub fn new(id_salt: impl Into<Id>, items: &'a [T], selected: &'a mut String) -> Self {
Self {
id: id_salt.into(),
items,
suggestions: items,
selected,
}
}
Expand All @@ -107,15 +120,15 @@ impl<'a, 'b, T: ToString + Debug + std::hash::Hash> CompletionEdit<'a, 'b, T> {
) -> Response {
let matching_items = ui.memory_mut(|writer| {
let cache = writer.caches.cache::<CachedMatcherSearch>();
cache.get((&state.current_search, self.items))
cache.get((self.selected, self.suggestions))
});

let pressed_down =
ui.input_mut(|reader| reader.consume_key(Modifiers::NONE, Key::ArrowDown));
let pressed_up = ui.input_mut(|reader| reader.consume_key(Modifiers::NONE, Key::ArrowUp));

let mut edit_output = match state.user_state {
UserState::Typing => TextEdit::singleline(&mut state.current_search)
let TextEditOutput {
mut response,
state: mut text_edit_state,
..
} = match state.user_state {
UserState::Typing => TextEdit::singleline(self.selected)
.hint_text("Search")
.show(ui),
UserState::Selecting { index } => {
Expand All @@ -127,48 +140,43 @@ impl<'a, 'b, T: ToString + Debug + std::hash::Hash> CompletionEdit<'a, 'b, T> {
.hint_text("Search")
.show(ui);
if output.response.changed() {
state.current_search = selected;
*self.selected = selected;
}
output
}
};
response.changed = false;

if edit_output.response.changed() {
state.user_state = UserState::Typing;
if !response.has_focus() {
return response;
}

let pressed_down = ui.input_mut(|reader| reader.key_pressed(Key::ArrowDown));
let pressed_up = ui.input_mut(|reader| reader.key_pressed(Key::ArrowUp));
if pressed_down || pressed_up {
// Set the cursor to the right of the new word
edit_output
.state
text_edit_state
.cursor
.set_char_range(Some(CCursorRange::one(CCursor::new(usize::MAX))));
edit_output.state.store(ui.ctx(), edit_output.response.id);
text_edit_state.store(ui.ctx(), response.id);
}
state.user_state =
state
.user_state
.handle_arrow(pressed_down, pressed_up, matching_items.len());

state.user_state = match (pressed_up, pressed_down, state.user_state) {
(_, true, UserState::Typing) => UserState::Selecting { index: 0 },
(true, _, UserState::Selecting { index: 0 }) => UserState::Typing,
(true, _, UserState::Selecting { index }) => UserState::Selecting { index: index - 1 },
(_, true, UserState::Selecting { index }) => UserState::Selecting {
index: (index + 1).min(matching_items.len() - 1),
},
(_, _, state) => state,
};
if matching_items.is_empty() {
state.user_state = UserState::Typing;
}

let selection_may_have_changed =
edit_output.response.changed() || pressed_down || pressed_up;

let popup_id = self.id.with("popup");
let text_size = ui.text_style_height(&TextStyle::Body);

let selection_may_have_changed = response.changed() || pressed_down || pressed_up;
let should_close_popup = popup_below_widget(
ui,
popup_id,
&edit_output.response,
&response,
PopupCloseBehavior::CloseOnClickOutside,
|ui| {
let mut close_me = false;
Expand All @@ -188,7 +196,8 @@ impl<'a, 'b, T: ToString + Debug + std::hash::Hash> CompletionEdit<'a, 'b, T> {
UserState::Typing => false,
};

let response = show_value(ui, highlight, &self.items[*original_index]);
let response =
show_value(ui, highlight, &self.suggestions[*original_index]);

if selection_may_have_changed && highlight {
response.scroll_to_me(None);
Expand All @@ -207,30 +216,33 @@ impl<'a, 'b, T: ToString + Debug + std::hash::Hash> CompletionEdit<'a, 'b, T> {
},
);

let gained_focus = edit_output.response.gained_focus();
let close_popup = matches!(should_close_popup, Some(true))
|| edit_output.response.lost_focus()
&& ui.input(|reader| reader.key_pressed(Key::Enter));
let has_focus = response.has_focus();
let user_completed_search = matches!(should_close_popup, Some(true))
|| response.lost_focus() && ui.input(|reader| reader.key_pressed(Key::Enter));

ui.memory_mut(|memory| {
if gained_focus {
memory.toggle_popup(popup_id);
if has_focus {
memory.open_popup(popup_id);
}
if close_popup {
if user_completed_search {
memory.close_popup();
}
});

if let UserState::Selecting { index } = state.user_state {
let (actual_index, _) = matching_items[index];
*self.selected = self.items.get(actual_index);
if user_completed_search {
response.mark_changed();
if let UserState::Selecting { index } = state.user_state {
let (actual_index, _) = matching_items[index];
*self.selected = self.suggestions[actual_index].to_string();
state.user_state = UserState::Typing;
}
}

edit_output.response
response
}
}

impl<'a, 'b, T: ToString + Debug + std::hash::Hash> Widget for CompletionEdit<'a, 'b, T> {
impl<'a, T: Clone + ToString + Debug + std::hash::Hash> Widget for CompletionEdit<'a, T> {
fn ui(self, ui: &mut Ui) -> Response {
self.ui(ui, |ui, highlight, item| {
ui.selectable_label(highlight, item.to_string())
Expand Down
2 changes: 2 additions & 0 deletions crates/hulk_widgets/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
mod completion_edit;
mod nao_path_completion_edit;
mod segmented_control;

pub use completion_edit::CompletionEdit;
pub use nao_path_completion_edit::{NaoPathCompletionEdit, PathFilter};
pub use segmented_control::SegmentedControl;
53 changes: 53 additions & 0 deletions crates/hulk_widgets/src/nao_path_completion_edit.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
use crate::CompletionEdit;

use communication::client::PathsEvent;
use egui::{Id, Response, Ui, Widget};

pub enum PathFilter {
Readable,
Writable,
}

pub struct NaoPathCompletionEdit<'ui> {
id: Id,
paths_events: PathsEvent,
path: &'ui mut String,
filter: PathFilter,
}

impl<'ui> NaoPathCompletionEdit<'ui> {
pub fn new(
id_salt: impl Into<Id>,
paths_events: PathsEvent,
path: &'ui mut String,
filter: PathFilter,
) -> Self {
Self {
id: id_salt.into(),
paths_events,
path,
filter,
}
}

fn list_paths(&self) -> Vec<String> {
match self.paths_events.as_ref() {
Some(Ok(paths)) => paths
.iter()
.filter_map(|(path, entry)| match self.filter {
PathFilter::Readable if entry.is_readable => Some(path.clone()),
PathFilter::Writable if entry.is_writable => Some(path.clone()),
_ => None,
})
.collect(),
_ => Vec::new(),
}
}
}

impl<'ui> Widget for NaoPathCompletionEdit<'ui> {
fn ui(self, ui: &mut Ui) -> Response {
let paths = self.list_paths();
ui.add(CompletionEdit::new(self.id, &paths, self.path))
}
}
10 changes: 7 additions & 3 deletions crates/hulk_widgets/src/segmented_control.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,16 @@ impl<'ui, T: ToString> SegmentedControl<'ui, T> {
.rounding
.unwrap_or(ui.style().noninteractive().rounding);

let (response, painter) = ui.allocate_painter(vec2(width, 2.0 * text_size), Sense::hover());
let (mut response, painter) =
ui.allocate_painter(vec2(width, 2.0 * text_size), Sense::hover());
if response.contains_pointer() {
ui.input(|reader| {
if reader.key_pressed(Key::ArrowLeft) {
state.selected = state.selected.saturating_sub(1);
response.mark_changed();
} else if reader.key_pressed(Key::ArrowRight) {
state.selected = (state.selected + 1).min(self.selectables.len() - 1);
response.mark_changed();
}
})
}
Expand All @@ -76,7 +79,7 @@ impl<'ui, T: ToString> SegmentedControl<'ui, T> {
let noninteractive_style = ui.style().noninteractive();

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

let show_line = idx > 0 && state.selected != idx && state.selected + 1 != idx;
Expand All @@ -97,8 +100,9 @@ impl<'ui, T: ToString> SegmentedControl<'ui, T> {
);
}

if response.clicked() {
if label_response.clicked() {
state.selected = idx;
response.mark_changed();
}
painter.text(
rect.center(),
Expand Down
Loading

0 comments on commit 536aaec

Please sign in to comment.