diff --git a/Cargo.lock b/Cargo.lock index 169c93d1da..c10cbe7be7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -278,9 +278,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.15" +version = "0.6.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +checksum = "23a1e53f0f5d86382dafe1cf314783b2044280f406e7e1506368220ad11b1338" dependencies = [ "anstyle", "anstyle-parse", @@ -293,36 +293,36 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" +checksum = "8365de52b16c035ff4fcafe0092ba9390540e3e352870ac09933bebcaa2c8c56" [[package]] name = "anstyle-parse" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.4" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" dependencies = [ "anstyle", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -347,7 +347,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.85", ] [[package]] @@ -575,7 +575,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.85", ] [[package]] @@ -610,7 +610,7 @@ checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.85", ] [[package]] @@ -821,7 +821,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.82", + "syn 2.0.85", "which", ] @@ -844,7 +844,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.82", + "syn 2.0.85", "which", ] @@ -978,7 +978,7 @@ checksum = "bcfcc3cd946cb52f0bbfdbbcfa2f4e24f75ebb6c0e1002f7c25904fada18b9ec" dependencies = [ "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.85", ] [[package]] @@ -1175,9 +1175,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.33" +version = "4.5.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9646e2e245bf62f45d39a0f3f36f1171ad1ea0d6967fd114bca72cb02a8fcdfb" +checksum = "07a13ab5b8cb13dbe35e68b83f6c12f9293b2f601797b71bc9f23befdb329feb" dependencies = [ "clap 4.5.20", ] @@ -1191,7 +1191,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.85", ] [[package]] @@ -1240,7 +1240,7 @@ dependencies = [ "proc-macro2", "quote", "source_analyzer", - "syn 2.0.82", + "syn 2.0.85", "thiserror", ] @@ -1289,9 +1289,9 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] name = "colorchoice" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "colored" @@ -1434,7 +1434,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.85", ] [[package]] @@ -1677,7 +1677,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.82", + "syn 2.0.85", ] [[package]] @@ -2003,9 +2003,9 @@ checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" [[package]] name = "encoding_rs" -version = "0.8.34" +version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ "cfg-if", ] @@ -2043,7 +2043,7 @@ checksum = "a1ab991c1362ac86c61ab6f556cff143daa22e5a15e4e189df818b2fd19fe65b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.85", ] [[package]] @@ -2064,7 +2064,7 @@ checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.85", ] [[package]] @@ -2076,7 +2076,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.85", ] [[package]] @@ -2097,7 +2097,7 @@ checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.85", ] [[package]] @@ -2108,7 +2108,7 @@ checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38" dependencies = [ "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.85", ] [[package]] @@ -2368,7 +2368,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.85", ] [[package]] @@ -2460,7 +2460,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.85", ] [[package]] @@ -2492,15 +2492,6 @@ dependencies = [ "slab", ] -[[package]] -name = "fuzzy-matcher" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" -dependencies = [ - "thread_local", -] - [[package]] name = "generic-array" version = "0.14.7" @@ -3176,6 +3167,7 @@ dependencies = [ name = "hulk_widgets" version = "0.1.0" dependencies = [ + "communication", "egui", "egui_extras", "nucleo-matcher", @@ -3588,9 +3580,9 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" +checksum = "3bda4c6077b0b08da2c48b172195795498381a7c8988c9e6212a6c55c5b9bd70" [[package]] name = "libredox" @@ -3891,7 +3883,7 @@ checksum = "254a5372af8fc138e36684761d3c0cdb758a4410e938babcff1c860ce14ddbfc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.85", ] [[package]] @@ -4105,7 +4097,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.85", ] [[package]] @@ -4155,7 +4147,7 @@ dependencies = [ "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.85", ] [[package]] @@ -4459,7 +4451,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.85", ] [[package]] @@ -4668,7 +4660,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.85", ] [[package]] @@ -4709,29 +4701,29 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project" -version = "1.1.6" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf123a161dde1e524adf36f90bc5d8d3462824a9c43553ad07a8183161189ec" +checksum = "be57f64e946e500c8ee36ef6331845d40a93055567ec57e8fae13efd33759b95" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.6" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4502d8515ca9f32f1fb543d987f63d95a14934883db45bdb48060b6b69257f8" +checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.85", ] [[package]] name = "pin-project-lite" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" [[package]] name = "pin-utils" @@ -4842,12 +4834,12 @@ checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" [[package]] name = "prettyplease" -version = "0.2.24" +version = "0.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "910d41a655dac3b764f1ade94821093d3610248694320cd072303a8eedcf221d" +checksum = "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033" dependencies = [ "proc-macro2", - "syn 2.0.82", + "syn 2.0.85", ] [[package]] @@ -4919,7 +4911,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.85", "version_check", "yansi", ] @@ -5107,9 +5099,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", @@ -5398,7 +5390,7 @@ checksum = "7e85ad2009c50b58e87caa8cd6dac16bdf511bbfb7af6c33df902396aa480fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.85", ] [[package]] @@ -5430,7 +5422,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.85", ] [[package]] @@ -5634,7 +5626,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.82", + "syn 2.0.85", "thiserror", "threadbound", "toposort-scc", @@ -5774,9 +5766,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.82" +version = "2.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83540f837a8afc019423a8edb95b52a8effe46957ee402287f4292fae35be021" +checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" dependencies = [ "proc-macro2", "quote", @@ -5893,7 +5885,7 @@ checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602" dependencies = [ "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.85", ] [[package]] @@ -6020,7 +6012,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.85", ] [[package]] @@ -6134,7 +6126,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.85", ] [[package]] @@ -6231,9 +6223,9 @@ dependencies = [ "egui_plot", "fern", "framework", - "fuzzy-matcher", "geometry", "gilrs", + "hulk_widgets", "image 0.24.9", "itertools 0.10.5", "kinematics", @@ -6502,7 +6494,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.85", "wasm-bindgen-shared", ] @@ -6536,7 +6528,7 @@ checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" dependencies = [ "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.85", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -6944,7 +6936,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.85", ] [[package]] @@ -6955,7 +6947,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.85", ] [[package]] @@ -7456,7 +7448,7 @@ checksum = "709ab20fc57cb22af85be7b360239563209258430bccf38d8b979c5a2ae3ecce" dependencies = [ "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.85", "zbus-lockstep", "zbus_xml", "zvariant 4.2.0", @@ -7485,7 +7477,7 @@ dependencies = [ "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.85", "zvariant_utils 2.1.0", ] @@ -7542,7 +7534,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.85", ] [[package]] @@ -7603,7 +7595,7 @@ dependencies = [ "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.85", "zvariant_utils 2.1.0", ] @@ -7626,7 +7618,7 @@ checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" dependencies = [ "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.85", ] [[patch.unused]] diff --git a/Cargo.toml b/Cargo.toml index 8d70565eb2..f2d2233a68 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/crates/hulk_replayer/src/labels.rs b/crates/hulk_replayer/src/labels.rs index 8b87be680f..e70b6e6a17 100644 --- a/crates/hulk_replayer/src/labels.rs +++ b/crates/hulk_replayer/src/labels.rs @@ -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; @@ -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( diff --git a/crates/hulk_replayer/src/timeline.rs b/crates/hulk_replayer/src/timeline.rs index 976c863441..59af8b78c9 100644 --- a/crates/hulk_replayer/src/timeline.rs +++ b/crates/hulk_replayer/src/timeline.rs @@ -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; @@ -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( @@ -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 }) diff --git a/crates/hulk_widgets/Cargo.toml b/crates/hulk_widgets/Cargo.toml index edf4cb9196..18b4ce20f3 100644 --- a/crates/hulk_widgets/Cargo.toml +++ b/crates/hulk_widgets/Cargo.toml @@ -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 } diff --git a/crates/hulk_widgets/src/completion_edit.rs b/crates/hulk_widgets/src/completion_edit.rs index a0d59464ec..b15c977764 100644 --- a/crates/hulk_widgets/src/completion_edit.rs +++ b/crates/hulk_widgets/src/completion_edit.rs @@ -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)] @@ -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, } @@ -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, 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, items: &'a [T], selected: &'a mut String) -> Self { Self { id: id_salt.into(), - items, + suggestions: items, selected, } } @@ -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::(); - 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 } => { @@ -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; @@ -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); @@ -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()) diff --git a/crates/hulk_widgets/src/lib.rs b/crates/hulk_widgets/src/lib.rs index a794d0d518..2261a221a4 100644 --- a/crates/hulk_widgets/src/lib.rs +++ b/crates/hulk_widgets/src/lib.rs @@ -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; diff --git a/crates/hulk_widgets/src/nao_path_completion_edit.rs b/crates/hulk_widgets/src/nao_path_completion_edit.rs new file mode 100644 index 0000000000..658160dfdb --- /dev/null +++ b/crates/hulk_widgets/src/nao_path_completion_edit.rs @@ -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, + paths_events: PathsEvent, + path: &'ui mut String, + filter: PathFilter, + ) -> Self { + Self { + id: id_salt.into(), + paths_events, + path, + filter, + } + } + + fn list_paths(&self) -> Vec { + 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)) + } +} diff --git a/crates/hulk_widgets/src/segmented_control.rs b/crates/hulk_widgets/src/segmented_control.rs index b91c120481..90e8f8c92f 100644 --- a/crates/hulk_widgets/src/segmented_control.rs +++ b/crates/hulk_widgets/src/segmented_control.rs @@ -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(); } }) } @@ -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; @@ -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(), diff --git a/tools/annotato/src/widgets/class_selector.rs b/tools/annotato/src/widgets/class_selector.rs index 331b754dd8..d9c4a19cbf 100644 --- a/tools/annotato/src/widgets/class_selector.rs +++ b/tools/annotato/src/widgets/class_selector.rs @@ -29,7 +29,7 @@ impl<'a> Widget for ClassSelector<'a> { *self.currently_selected = class; } - ComboBox::from_id_source(self.id) + ComboBox::from_id_salt(self.id) .selected_text(format!("{:?}", self.currently_selected)) .show_ui(ui, |ui| { Class::list().into_iter().for_each(|class| { diff --git a/tools/twix/Cargo.toml b/tools/twix/Cargo.toml index e1cdfa71db..0f2b6bf8f2 100644 --- a/tools/twix/Cargo.toml +++ b/tools/twix/Cargo.toml @@ -24,7 +24,6 @@ egui_extras = { workspace = true } egui_plot = { workspace = true } fern = { workspace = true } framework = { workspace = true } -fuzzy-matcher = { workspace = true } geometry = { workspace = true } gilrs = { workspace = true } image = { workspace = true } @@ -44,3 +43,4 @@ tokio = { workspace = true } toml = { workspace = true } types = { workspace = true } walking_engine = { workspace = true } +hulk_widgets = { workspace = true } diff --git a/tools/twix/config_default.toml b/tools/twix/config_default.toml index 565f40b3a4..c5e2aaf084 100644 --- a/tools/twix/config_default.toml +++ b/tools/twix/config_default.toml @@ -20,3 +20,6 @@ C-Right = "focus_right" C-w = "close_tab" C-d = "duplicate_tab" +[naos] +lowest = 21 +highest = 41 diff --git a/tools/twix/src/completion_edit.rs b/tools/twix/src/completion_edit.rs deleted file mode 100644 index 57ec1094d6..0000000000 --- a/tools/twix/src/completion_edit.rs +++ /dev/null @@ -1,274 +0,0 @@ -use std::{ - iter::once, - net::{IpAddr, Ipv4Addr}, - ops::RangeInclusive, -}; - -use eframe::{ - egui::{ - text::{CCursor, CCursorRange}, - Area, Context, Frame, Id, Key, Modifiers, Order, Response, ScrollArea, TextEdit, Ui, - Widget, WidgetText, - }, - epaint::Color32, -}; -use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher}; -use itertools::chain; -use log::error; - -use crate::nao::Nao; - -#[derive(Default, Clone, Copy)] -struct CompletionState { - selected_item: Option, -} - -impl CompletionState { - fn load(ctx: &Context, id: Id) -> Option { - ctx.data_mut(|data| data.get_temp(id)) - } - - fn store(self, ctx: &Context, id: Id) { - ctx.data_mut(|data| data.insert_temp(id, self)); - } -} - -pub struct CompletionEntry { - text: String, - highlight: bool, -} - -impl CompletionEntry { - pub fn new(text: String, highlight: bool) -> Self { - Self { text, highlight } - } -} - -impl From for CompletionEntry { - fn from(value: String) -> Self { - Self::new(value, false) - } -} - -pub struct CompletionEdit<'key> { - hint_text: WidgetText, - key: &'key mut String, - completion_items: Vec, -} - -impl<'key> CompletionEdit<'key> { - pub fn new( - key: &'key mut String, - completion_items: Vec, - hint_text: impl Into, - ) -> Self { - Self { - hint_text: hint_text.into(), - key, - completion_items, - } - } - - pub fn addresses( - key: &'key mut String, - numbers: RangeInclusive, - highlighted_ips: &[IpAddr], - ) -> Self { - let completion_items: Vec<_> = chain!( - once(CompletionEntry::new("localhost".to_string(), true)), - numbers.clone().map(|number| { - let ip = IpAddr::V4(Ipv4Addr::new(10, 1, 24, number)); - CompletionEntry::new(ip.to_string(), highlighted_ips.contains(&ip)) - }), - numbers.map(|number| CompletionEntry::new(format!("10.0.24.{number}"), false)) - ) - .collect(); - - Self { - hint_text: "Address".into(), - key, - completion_items, - } - } - - pub fn readable_paths(key: &'key mut String, nao: &Nao) -> Self { - let completion_items = match &*nao.latest_paths() { - Some(Ok(paths)) => paths - .iter() - .filter_map(|(path, entry)| { - if entry.is_readable { - Some(CompletionEntry::from(path.clone())) - } else { - None - } - }) - .collect(), - Some(Err(error)) => { - error!("{error}"); - Vec::new() - } - None => Vec::new(), - }; - - Self { - hint_text: "Path".into(), - key, - completion_items, - } - } - - pub fn writable_paths(key: &'key mut String, nao: &Nao) -> Self { - let completion_items = match &*nao.latest_paths() { - Some(Ok(paths)) => paths - .iter() - .filter_map(|(path, entry)| { - if entry.is_writable { - Some(CompletionEntry::from(path.clone())) - } else { - None - } - }) - .collect(), - Some(Err(error)) => { - error!("{error}"); - Vec::new() - } - None => Vec::new(), - }; - - Self { - hint_text: "Path".into(), - key, - completion_items, - } - } - - pub fn select_all(text: &str, ui: &mut Ui, id: Id) { - if let Some(mut state) = TextEdit::load_state(ui.ctx(), id) { - state.cursor.set_char_range(Some(CCursorRange::two( - CCursor::new(0), - CCursor::new(text.chars().count()), - ))); - TextEdit::store_state(ui.ctx(), id, state); - } - } -} - -impl Widget for CompletionEdit<'_> { - fn ui(self, ui: &mut Ui) -> Response { - let mut response = TextEdit::singleline(self.key) - .hint_text(self.hint_text) - .lock_focus(true) - .ui(ui); - - let popup_id = response.id.with("completion_popup"); - let is_open = ui.memory(|memory| memory.is_popup_open(popup_id)); - let mut state = CompletionState::load(ui.ctx(), popup_id).unwrap_or_default(); - let matcher = SkimMatcherV2::default(); - let mut completion_text_items: Vec<_> = self - .completion_items - .into_iter() - .filter_map(|item| { - matcher - .fuzzy_match(&item.text, self.key) - .map(|score| (score, item)) - }) - .collect(); - completion_text_items.sort_by_key(|(score, _)| -*score); - - if response.has_focus() != is_open { - ui.memory_mut(|memory| memory.toggle_popup(popup_id)); - if response.gained_focus() { - CompletionEdit::select_all(self.key, ui, response.id); - } - } - if response.changed() { - state.selected_item = if completion_text_items.is_empty() { - None - } else { - Some(0) - }; - } - response.changed = false; - - if is_open { - if !completion_text_items.is_empty() { - ui.input_mut(|input| { - if input.consume_key(Modifiers::NONE, Key::ArrowDown) - || input.consume_key(Modifiers::NONE, Key::Tab) - { - state.selected_item = Some( - (state.selected_item.unwrap_or(-1) + 1) - % (completion_text_items.len() as i64), - ); - } else if input.consume_key(Modifiers::NONE, Key::ArrowUp) - || input.consume_key(eframe::egui::Modifiers::SHIFT, Key::Tab) - { - state.selected_item = Some( - (state - .selected_item - .unwrap_or(completion_text_items.len() as i64) - - 1) - .rem_euclid(completion_text_items.len() as i64), - ); - } - }); - } else { - state.selected_item = None; - } - - if ui.input(|input| input.key_pressed(Key::Enter)) { - if state.selected_item.is_some() { - *self.key = completion_text_items - .get(state.selected_item.unwrap() as usize) - .unwrap() - .1 - .text - .to_string(); - state.selected_item = None; - } - response.mark_changed(); - } - let area = Area::new(popup_id) - .order(Order::Foreground) - .current_pos(response.rect.left_bottom()) - .show(ui.ctx(), |ui| { - Frame::popup(ui.style()).show(ui, |ui| { - ScrollArea::vertical().show(ui, |ui| { - for (i, completion_item) in - completion_text_items.into_iter().enumerate() - { - let completion_entry = &completion_item.1; - let is_selected = Some(i as i64) == state.selected_item; - - let mut text = WidgetText::from(completion_entry.text.clone()); - if completion_entry.highlight { - text = text.color(Color32::GREEN); - } - - let label = ui.selectable_label(is_selected, text); - - if is_selected { - label.scroll_to_me(None); - } - if label.is_pointer_button_down_on() { - self.key.clone_from(&completion_item.1.text); - response.mark_changed(); - ui.memory_mut(|memory| memory.close_popup()); - } - } - }); - }) - }); - if ui.input(|input| input.key_pressed(Key::Escape)) - || response.union(area.response).clicked_elsewhere() - { - ui.memory_mut(|memory| memory.close_popup()); - } - } else { - state.selected_item = None; - } - state.store(ui.ctx(), popup_id); - response - } -} diff --git a/tools/twix/src/configuration.rs b/tools/twix/src/configuration.rs index 030d52a014..c85571b787 100644 --- a/tools/twix/src/configuration.rs +++ b/tools/twix/src/configuration.rs @@ -27,6 +27,14 @@ fn config_path() -> PathBuf { #[derive(Debug, Deserialize)] pub struct Configuration { pub keys: keys::Keybinds, + pub naos: NaoConfig, +} + +#[cfg_attr(test, derive(PartialEq))] +#[derive(Debug, Deserialize)] +pub struct NaoConfig { + pub lowest: u8, + pub highest: u8, } impl Configuration { @@ -56,7 +64,7 @@ impl Configuration { } pub fn merge(&mut self, other: Self) { - let Self { keys } = other; + let Self { keys, .. } = other; self.keys.merge(keys); } diff --git a/tools/twix/src/configuration/keybind_plugin.rs b/tools/twix/src/configuration/keybind_plugin.rs index 36d147d703..61b6d9bc5e 100644 --- a/tools/twix/src/configuration/keybind_plugin.rs +++ b/tools/twix/src/configuration/keybind_plugin.rs @@ -7,7 +7,7 @@ use super::keys::{KeybindAction, Keybinds}; type ActionList = Arc>; pub fn register(ctx: &Context) { - ctx.on_begin_frame("keybinds", Arc::new(begin_frame)) + ctx.on_begin_pass("keybinds", Arc::new(begin_frame)) } fn begin_frame(ctx: &Context) { diff --git a/tools/twix/src/main.rs b/tools/twix/src/main.rs index 0b2d92c187..56738bdaa4 100644 --- a/tools/twix/src/main.rs +++ b/tools/twix/src/main.rs @@ -1,12 +1,5 @@ -use std::{ - fmt::{self, Display, Formatter}, - net::IpAddr, - str::FromStr, - sync::Arc, - time::{Duration, SystemTime}, -}; +use std::{iter::once, net::Ipv4Addr, str::FromStr, sync::Arc, time::SystemTime}; -use aliveness::query_aliveness; use argument_parsers::NaoAddress; use clap::Parser; use color_eyre::{ @@ -15,7 +8,6 @@ use color_eyre::{ }; use communication::client::Status; -use completion_edit::CompletionEdit; use configuration::{ keybind_plugin::{self, KeybindSystem}, keys::KeybindAction, @@ -30,6 +22,8 @@ use eframe::{ use egui_dock::{DockArea, DockState, Node, NodeIndex, Split, SurfaceIndex, TabAddAlign, TabIndex}; use fern::{colors::ColoredLevelConfig, Dispatch, InitError}; +use hulk_widgets::CompletionEdit; +use itertools::chain; use log::error; use nao::Nao; use panel::Panel; @@ -39,22 +33,20 @@ use panels::{ RemotePanel, TextPanel, VisionTunerPanel, }; +use reachable_naos::ReachableNaos; use repository::{get_repository_root, Repository}; use serde_json::{from_str, to_string, Value}; -use tokio::{ - runtime::{Builder, Runtime}, - sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, -}; +use tokio::runtime::Runtime; use visuals::Visuals; mod change_buffer; -mod completion_edit; mod configuration; mod log_error; mod nao; mod panel; mod panels; mod players_buffer_handle; +mod reachable_naos; mod selectable_panel_macro; mod twix_painter; mod value_buffer; @@ -115,48 +107,6 @@ fn main() -> Result<(), eframe::Error> { ) } -struct ReachableNaos { - ips: Vec, - tx: UnboundedSender>, - rx: UnboundedReceiver>, - context: Context, - runtime: Runtime, -} - -impl ReachableNaos { - pub fn new(context: Context) -> Self { - let ips = Vec::new(); - let (tx, rx) = unbounded_channel(); - let runtime = Builder::new_multi_thread().enable_all().build().unwrap(); - - Self { - ips, - tx, - rx, - context, - runtime, - } - } - - pub fn query_reachability(&self) { - let tx = self.tx.clone(); - let context = self.context.clone(); - self.runtime.spawn(async move { - if let Ok(ips) = query_aliveness(Duration::from_millis(200), None).await { - let ips = ips.into_iter().map(|(ip, _)| ip).collect(); - let _ = tx.send(ips); - context.request_repaint(); - } - }); - } - - pub fn update(&mut self) { - while let Ok(ips) = self.rx.try_recv() { - self.ips = ips; - } - } -} - impl_selectable_panel!( BallCandidatePanel, BehaviorSimulatorPanel, @@ -173,11 +123,13 @@ impl_selectable_panel!( VisionTunerPanel, ImageColorSelectPanel, ); + struct TwixApp { nao: Arc, + possible_addresses: Vec, + address: String, reachable_naos: ReachableNaos, connection_intent: bool, - address: String, panel_selection: String, last_focused_tab: (NodeIndex, TabIndex), dock_state: DockState, @@ -190,15 +142,22 @@ impl TwixApp { arguments: Arguments, configuration: Configuration, ) -> Self { + let nao_range = configuration.naos.lowest..=configuration.naos.highest; + let possible_addresses: Vec<_> = chain!( + once(Ipv4Addr::LOCALHOST), + nao_range.clone().map(|id| Ipv4Addr::new(10, 0, 24, id)), + nao_range.map(|id| Ipv4Addr::new(10, 1, 24, id)), + ) + .collect(); let address = arguments .address - .map(|address| { + .and_then(|address| { NaoAddress::from_str(&address) - .map(|nao| nao.to_string()) - .unwrap_or(address) + .map(|nao| nao.ip.to_string()) + .ok() }) .or_else(|| creation_context.storage?.get_string("address")) - .unwrap_or_else(|| "localhost".to_string()); + .unwrap_or(Ipv4Addr::LOCALHOST.to_string()); let nao = Arc::new(Nao::new(format!("ws://{address}:1337"))); @@ -255,11 +214,12 @@ impl TwixApp { nao, reachable_naos, connection_intent, - address, panel_selection, dock_state, last_focused_tab: (0.into(), 0.into()), visual, + possible_addresses, + address, } } @@ -379,18 +339,26 @@ impl App for TwixApp { TopBottomPanel::top("top_bar").show(context, |ui| { ui.horizontal(|ui| { ui.with_layout(Layout::left_to_right(Align::Center), |ui| { - let address_input = CompletionEdit::addresses( + let address_input = CompletionEdit::new( + ui.id().with("nao-selector"), + &self.possible_addresses, &mut self.address, - 21..=41, - &self.reachable_naos.ips, ) - .ui(ui); + .ui(ui, |ui, selected, ip| { + let show_green = self.reachable_naos.is_reachable(*ip); + let color = if show_green { + Color32::GREEN + } else { + Color32::WHITE + }; + ui.selectable_label(selected, WidgetText::from(ip.to_string()).color(color)) + }); + if address_input.gained_focus() { self.reachable_naos.query_reachability(); } if context.keybind_pressed(KeybindAction::FocusAddress) { address_input.request_focus(); - CompletionEdit::select_all(&self.address, ui, address_input.id); } if address_input.changed() || address_input.lost_focus() { let address = &self.address; @@ -427,18 +395,15 @@ impl App for TwixApp { self.panel_selection = name } } - let panel_input = CompletionEdit::new( + let panels = SelectablePanel::registered(); + let panel_input = ui.add(CompletionEdit::new( + ui.id().with("panel-selector"), + &panels, &mut self.panel_selection, - SelectablePanel::registered() - .into_iter() - .map(|registered| registered.into()) - .collect(), - "Panel", - ) - .ui(ui); + )); + if context.keybind_pressed(KeybindAction::FocusPanel) { panel_input.request_focus(); - CompletionEdit::select_all(&self.panel_selection, ui, panel_input.id); } if panel_input.changed() || panel_input.lost_focus() { match SelectablePanel::try_from_name( @@ -568,7 +533,7 @@ impl App for TwixApp { ui.painter().rect_stroke( rect, Rounding::same(4.0), - ui.style().visuals.widgets.active.bg_stroke, + ui.visuals().widgets.active.bg_stroke, ); } }); @@ -578,7 +543,7 @@ impl App for TwixApp { let dock_state = self.dock_state.map_tabs(|tab| tab.panel.save()); storage.set_string("dock_state", to_string(&dock_state).unwrap()); - storage.set_string("address", self.address.clone()); + storage.set_string("address", self.address.to_string()); storage.set_string( "connection_intent", if self.connection_intent { diff --git a/tools/twix/src/panels/behavior_simulator.rs b/tools/twix/src/panels/behavior_simulator.rs index fe0825ab20..ba0bbe39fd 100644 --- a/tools/twix/src/panels/behavior_simulator.rs +++ b/tools/twix/src/panels/behavior_simulator.rs @@ -2,6 +2,7 @@ use std::sync::Arc; use communication::messages::TextOrBinary; use eframe::egui::{Color32, Response, Slider, Ui, Widget}; +use hulk_widgets::SegmentedControl; use serde_json::{json, Value}; use crate::{nao::Nao, panel::Panel, value_buffer::BufferHandle}; @@ -98,15 +99,10 @@ impl Widget for &mut BehaviorSimulatorPanel { }; }); ui.horizontal(|ui| { - if ui - .add_sized( - ui.available_size(), - Slider::new(&mut self.selected_robot, 1..=7) - .smart_aim(false) - .text("Robot"), - ) - .changed() - { + let robots = (1..=7).collect::>(); + let robot_selection = SegmentedControl::new("robot-selector", &robots).ui(ui); + self.selected_robot = *robot_selection.inner; + if robot_selection.response.changed() { self.nao.write( "parameters.selected_robot", TextOrBinary::Text(self.selected_robot.into()), diff --git a/tools/twix/src/panels/enum_plot.rs b/tools/twix/src/panels/enum_plot.rs index fcbca7b9c4..06e13e616a 100644 --- a/tools/twix/src/panels/enum_plot.rs +++ b/tools/twix/src/panels/enum_plot.rs @@ -18,9 +18,10 @@ use eframe::{ use itertools::Itertools; use serde_json::{json, Value}; +use hulk_widgets::{NaoPathCompletionEdit, PathFilter}; + use crate::{ change_buffer::{Change, ChangeBufferHandle}, - completion_edit::CompletionEdit, nao::Nao, panel::Panel, }; @@ -154,8 +155,12 @@ impl SegmentRow { } fn show_settings(&mut self, ui: &mut Ui, nao: Arc) { - let subscription_field = - ui.add(CompletionEdit::readable_paths(&mut self.path, nao.as_ref())); + let subscription_field = ui.add(NaoPathCompletionEdit::new( + ui.next_auto_id().with(ui.id()).with("enum-plot"), + nao.latest_paths(), + &mut self.path, + PathFilter::Readable, + )); if subscription_field.changed() { self.subscribe(nao); @@ -423,8 +428,7 @@ impl Widget for &mut EnumPlotPanel { Button::new(RichText::new("❌").color(Color32::WHITE).strong()) .fill(Color32::RED), ); - - segment_data.show_settings(ui, self.nao.clone()); + ui.scope(|ui| segment_data.show_settings(ui, self.nao.clone())); !delete_button.clicked() }) .inner diff --git a/tools/twix/src/panels/image_color_select.rs b/tools/twix/src/panels/image_color_select.rs index ce162d40a2..1bd475b5ab 100644 --- a/tools/twix/src/panels/image_color_select.rs +++ b/tools/twix/src/panels/image_color_select.rs @@ -114,7 +114,7 @@ impl Widget for &mut ImageColorSelectPanel { .show_inside(ui, |ui| { ui.horizontal(|ui| { ui.label("x:"); - ComboBox::from_id_source("x_axis") + ComboBox::from_id_salt("x_axis") .selected_text(format!("{:?}", self.x_axis)) .show_ui(ui, |ui| { ui.selectable_value(&mut self.x_axis, Axis::Cb, "Cb"); @@ -144,7 +144,7 @@ impl Widget for &mut ImageColorSelectPanel { ui.selectable_value(&mut self.x_axis, Axis::Saturation, "Saturation"); }); ui.label("y:"); - ComboBox::from_id_source("y_axis") + ComboBox::from_id_salt("y_axis") .selected_text(format!("{:?}", self.y_axis)) .show_ui(ui, |ui| { ui.selectable_value(&mut self.y_axis, Axis::Cb, "Cb"); diff --git a/tools/twix/src/panels/map/mod.rs b/tools/twix/src/panels/map/mod.rs index 31dc31b235..83af3800fe 100644 --- a/tools/twix/src/panels/map/mod.rs +++ b/tools/twix/src/panels/map/mod.rs @@ -193,7 +193,7 @@ impl Widget for &mut MapPanel { self.obstacle_filter.checkbox(ui); self.walking.checkbox(ui); }); - ComboBox::from_id_source("plot_type_selector") + ComboBox::from_id_salt("plot_type_selector") .selected_text(format!("{:?}", self.current_plot_type)) .show_ui(ui, |ui| { ui.selectable_value(&mut self.current_plot_type, PlotType::Ground, "Ground"); diff --git a/tools/twix/src/panels/parameter.rs b/tools/twix/src/panels/parameter.rs index f59594a6b9..7d5f7df512 100644 --- a/tools/twix/src/panels/parameter.rs +++ b/tools/twix/src/panels/parameter.rs @@ -1,15 +1,13 @@ use std::sync::Arc; -use crate::{ - completion_edit::CompletionEdit, log_error::LogError, nao::Nao, panel::Panel, - value_buffer::BufferHandle, -}; +use crate::{log_error::LogError, nao::Nao, panel::Panel, value_buffer::BufferHandle}; use color_eyre::{ eyre::{eyre, Error}, Result, }; use communication::messages::TextOrBinary; use eframe::egui::{Response, ScrollArea, TextEdit, Ui, Widget}; +use hulk_widgets::{NaoPathCompletionEdit, PathFilter}; use log::error; use parameters::directory::Scope; use serde_json::{json, Value}; @@ -49,8 +47,12 @@ impl Widget for &mut ParameterPanel { fn ui(self, ui: &mut Ui) -> Response { ui.vertical(|ui| { ui.horizontal(|ui| { - let path_edit = - CompletionEdit::writable_paths(&mut self.path, self.nao.as_ref()).ui(ui); + let path_edit = ui.add(NaoPathCompletionEdit::new( + ui.id().with("parameter"), + self.nao.latest_paths(), + &mut self.path, + PathFilter::Writable, + )); if path_edit.changed() { self.buffer = Some(self.nao.subscribe_json(&self.path)); } diff --git a/tools/twix/src/panels/plot.rs b/tools/twix/src/panels/plot.rs index d954d0fdc1..dd75f08158 100644 --- a/tools/twix/src/panels/plot.rs +++ b/tools/twix/src/panels/plot.rs @@ -9,12 +9,13 @@ use eframe::{ epaint::Color32, }; use egui_plot::{Line, Plot as EguiPlot, PlotPoints}; +use hulk_widgets::{NaoPathCompletionEdit, PathFilter}; use itertools::Itertools; use mlua::{Function, Lua, LuaSerdeExt}; use serde::{Deserialize, Serialize}; use serde_json::{json, to_string_pretty, Value}; -use crate::{completion_edit::CompletionEdit, nao::Nao, panel::Panel, value_buffer::BufferHandle}; +use crate::{nao::Nao, panel::Panel, value_buffer::BufferHandle}; const DEFAULT_LINE_COLORS: &[Color32] = &[ Color32::from_rgb(31, 119, 180), @@ -118,7 +119,12 @@ impl LineData { fn show_settings(&mut self, ui: &mut Ui, id: usize, nao: &Nao, buffer_history: Duration) { ui.horizontal_top(|ui| { - let subscription_field = ui.add(CompletionEdit::readable_paths(&mut self.path, nao)); + let subscription_field = ui.add(NaoPathCompletionEdit::new( + ui.id().with(id).with("plot-panel"), + nao.latest_paths(), + &mut self.path, + PathFilter::Readable, + )); self.set_highlighted(subscription_field.hovered()); if subscription_field.changed() { let handle = nao.subscribe_buffered_json(&self.path, buffer_history); @@ -127,9 +133,9 @@ impl LineData { ui.color_edit_button_srgba(&mut self.color); - let id_source = ui.id().with("conversion_collapse").with(id); + let id_salt = ui.id().with("conversion_collapse").with(id); CollapsingHeader::new("Conversion Function") - .id_source(id_source) + .id_salt(id_salt) .show(ui, |ui| { ui.horizontal(|ui| { let latest_value = self diff --git a/tools/twix/src/panels/text.rs b/tools/twix/src/panels/text.rs index f7ccd15293..633886cb28 100644 --- a/tools/twix/src/panels/text.rs +++ b/tools/twix/src/panels/text.rs @@ -2,9 +2,10 @@ use std::sync::Arc; use chrono::{DateTime, Utc}; use eframe::egui::{Label, Response, ScrollArea, Sense, Ui, Widget}; +use hulk_widgets::{NaoPathCompletionEdit, PathFilter}; use serde_json::{json, Value}; -use crate::{completion_edit::CompletionEdit, nao::Nao, panel::Panel, value_buffer::BufferHandle}; +use crate::{nao::Nao, panel::Panel, value_buffer::BufferHandle}; pub struct TextPanel { nao: Arc, @@ -39,9 +40,11 @@ impl Widget for &mut TextPanel { fn ui(self, ui: &mut Ui) -> Response { let edit_response = ui .horizontal(|ui| { - let edit_response = ui.add(CompletionEdit::readable_paths( + let edit_response = ui.add(NaoPathCompletionEdit::new( + ui.id().with("text-panel"), + self.nao.latest_paths(), &mut self.path, - self.nao.as_ref(), + PathFilter::Readable, )); if edit_response.changed() { self.buffer = Some(self.nao.subscribe_json(self.path.clone())); diff --git a/tools/twix/src/reachable_naos.rs b/tools/twix/src/reachable_naos.rs new file mode 100644 index 0000000000..420a3b3ffc --- /dev/null +++ b/tools/twix/src/reachable_naos.rs @@ -0,0 +1,54 @@ +use std::{net::IpAddr, time::Duration}; + +use aliveness::query_aliveness; +use eframe::egui::Context; +use tokio::{ + runtime::{Builder, Runtime}, + sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, +}; + +pub struct ReachableNaos { + ips: Vec, + tx: UnboundedSender>, + rx: UnboundedReceiver>, + context: Context, + runtime: Runtime, +} + +impl ReachableNaos { + pub fn new(context: Context) -> Self { + let ips = Vec::new(); + let (tx, rx) = unbounded_channel(); + let runtime = Builder::new_multi_thread().enable_all().build().unwrap(); + + Self { + ips, + tx, + rx, + context, + runtime, + } + } + + pub fn query_reachability(&self) { + let tx = self.tx.clone(); + let context = self.context.clone(); + self.runtime.spawn(async move { + if let Ok(ips) = query_aliveness(Duration::from_millis(200), None).await { + let ips = ips.into_iter().map(|(ip, _)| ip).collect(); + let _ = tx.send(ips); + context.request_repaint(); + } + }); + } + + pub fn update(&mut self) { + while let Ok(ips) = self.rx.try_recv() { + self.ips = ips; + } + } + + pub fn is_reachable(&self, ip: impl Into) -> bool { + self.ips.contains(&ip.into()) + } +} diff --git a/tools/twix/src/selectable_panel_macro.rs b/tools/twix/src/selectable_panel_macro.rs index 6d89394e2b..2b1c356ec8 100644 --- a/tools/twix/src/selectable_panel_macro.rs +++ b/tools/twix/src/selectable_panel_macro.rs @@ -59,8 +59,8 @@ macro_rules! impl_selectable_panel { } } - impl Display for SelectablePanel { - fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { + impl std::fmt::Display for SelectablePanel { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let panel_name = match self { $( SelectablePanel::$name(_) => $name::NAME, diff --git a/tools/widget_gallery/src/main.rs b/tools/widget_gallery/src/main.rs index 7b4eb405e5..43f918c113 100644 --- a/tools/widget_gallery/src/main.rs +++ b/tools/widget_gallery/src/main.rs @@ -1,58 +1,62 @@ use eframe::{ - egui::{CentralPanel, Context, Id}, - run_simple_native, Frame, + egui::{CentralPanel, Context}, + run_native, App, Frame, }; use hulk_widgets::{CompletionEdit, SegmentedControl}; fn main() -> eframe::Result { - run_simple_native("Gallery", Default::default(), show) + run_native( + "Gallery", + Default::default(), + Box::new(|_cc| Ok(Box::new(AppState::new()))), + ) } #[derive(Debug, Clone)] struct AppState { searchables: Vec, + selected: String, } impl AppState { pub fn new() -> Self { let searchables: Vec<_> = (1..100).map(|x| x.to_string()).collect(); - Self { searchables } + Self { + searchables, + selected: String::new(), + } } } -fn show(context: &Context, _frame: &mut Frame) { - let app_state = match context.data(|reader| reader.get_temp::(Id::NULL)) { - Some(app_state) => app_state, - None => { - let app_state = AppState::new(); - context.data_mut(|writer| writer.insert_temp(Id::NULL, app_state.clone())); - app_state - } - }; +impl App for AppState { + fn update(&mut self, context: &Context, _frame: &mut Frame) { + CentralPanel::default().show(context, |ui| { + ui.horizontal(|ui| { + let response = ui.add(CompletionEdit::new( + "completion-edit", + &self.searchables, + &mut self.selected, + )); + if response.changed() { + println!("Selected: {}", self.selected); + } - CentralPanel::default().show(context, |ui| { - let mut selected = None; - ui.horizontal(|ui| { - ui.add(CompletionEdit::new( - "completion-edit", - &app_state.searchables, - &mut selected, - )); - if let Some(selected) = selected { - ui.label(format!("Selected: {}", selected)); - } - }); + if ui.button("Focus").clicked() { + response.request_focus(); + } + }); - ui.separator(); + ui.separator(); - 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); + 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); + }) }) - }) - }); + }); + } }