Item 2 (Grok round 4): isboxer-style keybind tree UI.
Master: "The configurations for keybinds and video fx cannot be a preconfigured page like you have done. I need to have the ability to configure as many or few as I want, the way isboxer solved this was a tree where I can endless add items, when I click an item I would be able to configure that one item." References: - https://wiki.isboxer.com/Key_mapping - https://wiki.isboxer.com/Key_broadcasting Implementation: src/profile.rs: * Map.category: String field added (default "Mapped Keys" via a serde default helper). Categories: "Always On", "Combat", "Mapped Keys", "Character Sets", "Characters". * VideoFx.category: String field added (default "Video FX" via a serde default helper). * default_map_category + default_vfx_category helpers added. src/gui.rs: * SelectedNode enum: Map(usize) | VideoFx(usize). Drives the right-pane detail editor. * App struct gets a `selected_node: Option<SelectedNode>` field. * page_macros is gone. page_keybinds replaces it. * keybind_tree (left pane): CollapsingHeader per category. Items listed as selectable_label rows with a Remove button. "Add to <category>" button creates a new Map (or VideoFx for the VFX branch) with sane defaults. * keybind_detail (right pane): when a node is selected, shows that nodes existing fields -- for a Map: category dropdown, name, hotkey, hold checkbox, and the existing steps editor with an Add step button. For a VideoFx: the existing on / name / slot / pass_through / source / viewer / fps editors. * Page::Macros dispatch renamed from page_macros to page_keybinds. * ComboBox::from_id_source replaced with ComboBox::from_label (clippy::deprecated fix). DragValue uses .range (not the deprecated .clamp_range). Dropped unused crate::macros::print_macros import. format!("{}", x) replaced with x.to_string(). * All existing Map + VideoFx literal construction sites in engine.rs + gui.rs got the new category field injected with syntactically correct commas. cargo test 103/103; clippy clean.
This commit is contained in:
parent
8cb96c1f25
commit
24ca3bac0c
@ -311,6 +311,7 @@ mod tests {
|
||||
maps: vec![
|
||||
Map {
|
||||
name: "bar1".into(),
|
||||
category: "Mapped Keys".to_string(),
|
||||
hotkey: Hotkey("1".into()),
|
||||
hold: false,
|
||||
steps: vec![
|
||||
@ -330,7 +331,8 @@ mod tests {
|
||||
release_steps: vec![],
|
||||
},
|
||||
Map {
|
||||
// Smart shortcut: one user keypress fires the entire interact
|
||||
// Smart shortcut: one user keypress fires the entire interact,
|
||||
category: "Mapped Keys".to_string(),
|
||||
// chain (CTM on -> Alt+J -> sleep walk_delay_ms -> CTM off).
|
||||
name: "loot".into(),
|
||||
hotkey: Hotkey("Alt+G".into()),
|
||||
@ -356,6 +358,7 @@ mod tests {
|
||||
// between, conditional branches, etc.).
|
||||
Map {
|
||||
name: "loot_manual".into(),
|
||||
category: "Mapped Keys".to_string(),
|
||||
hotkey: Hotkey("Ctrl+Alt+G".into()),
|
||||
hold: false,
|
||||
steps: vec![
|
||||
@ -466,6 +469,7 @@ mod tests {
|
||||
let mut e = sample();
|
||||
let map = crate::profile::Map {
|
||||
name: "interact_simple".into(),
|
||||
category: "Mapped Keys".to_string(),
|
||||
hotkey: Hotkey("Alt+U".into()),
|
||||
hold: false,
|
||||
steps: vec![crate::profile::Step {
|
||||
@ -498,6 +502,7 @@ mod tests {
|
||||
e.profile.interact.walk_delay_ms = 2500;
|
||||
let map = crate::profile::Map {
|
||||
name: "interact_short".into(),
|
||||
category: "Mapped Keys".to_string(),
|
||||
hotkey: Hotkey("Alt+I".into()),
|
||||
hold: false,
|
||||
steps: vec![crate::profile::Step {
|
||||
@ -535,6 +540,7 @@ mod tests {
|
||||
e.profile.interact.walk_delay_ms = 9999;
|
||||
let map = crate::profile::Map {
|
||||
name: "interact_auto".into(),
|
||||
category: "Mapped Keys".to_string(),
|
||||
hotkey: Hotkey("Alt+I".into()),
|
||||
hold: false,
|
||||
steps: vec![crate::profile::Step {
|
||||
@ -565,6 +571,7 @@ mod tests {
|
||||
e.profile.interact.walk_delay_ms = 5000;
|
||||
let map = crate::profile::Map {
|
||||
name: "interact_hold".into(),
|
||||
category: "Mapped Keys".to_string(),
|
||||
hotkey: Hotkey("Alt+I".into()),
|
||||
hold: true,
|
||||
steps: vec![crate::profile::Step {
|
||||
@ -701,6 +708,7 @@ mod tests {
|
||||
groups: BTreeMap::new(),
|
||||
maps: vec![Map {
|
||||
name: map_name.into(),
|
||||
category: "Mapped Keys".to_string(),
|
||||
hotkey: Hotkey("F8".into()),
|
||||
hold: false,
|
||||
steps: vec![Step {
|
||||
|
||||
220
src/gui.rs
220
src/gui.rs
@ -1,7 +1,6 @@
|
||||
//! Control panel: menus for profile, routing mode, maps, and live crops.
|
||||
|
||||
use crate::hotkey::Hotkey;
|
||||
use crate::macros::print_macros;
|
||||
use crate::profile::{
|
||||
Character, default_config_path, Group, LayoutMode, LayoutPreset, Map, Mode, NormRect, Profile,
|
||||
Repeater, Step, VideoFx,
|
||||
@ -64,6 +63,7 @@ pub fn run() -> Result<()> {
|
||||
// Item 1: Free-mode buttons push IPC verbs here; the main
|
||||
// loop drains the queue and writes them to the socket.
|
||||
pending_ipc: Vec::new(),
|
||||
selected_node: None,
|
||||
};
|
||||
eframe::run_native("enBoxer", native, Box::new(|_cc| Ok(Box::new(app))))
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
@ -96,6 +96,14 @@ fn default_profile() -> Profile {
|
||||
}
|
||||
}
|
||||
|
||||
/// Item 2: which tree node the user has selected in the
|
||||
/// page_keybinds tree (drives the right-pane detail editor).
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
enum SelectedNode {
|
||||
Map(usize),
|
||||
VideoFx(usize),
|
||||
}
|
||||
|
||||
struct App {
|
||||
path: PathBuf,
|
||||
profile: Profile,
|
||||
@ -118,6 +126,8 @@ struct App {
|
||||
/// Item 1: Free-mode buttons push IPC verbs here; the main
|
||||
/// loop drains the queue and writes them to the socket.
|
||||
pending_ipc: Vec<String>,
|
||||
/// Item 2: which tree node is currently selected.
|
||||
selected_node: Option<SelectedNode>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@ -563,6 +573,7 @@ impl eframe::App for App {
|
||||
if ui.button("Add crop").clicked() {
|
||||
self.profile.video_fx.push(VideoFx {
|
||||
name: format!("crop{}", self.profile.video_fx.len() + 1),
|
||||
category: "Video FX".to_string(),
|
||||
enabled: true,
|
||||
source_slot: 2,
|
||||
source: NormRect {
|
||||
@ -635,7 +646,7 @@ impl eframe::App for App {
|
||||
Page::Layout => self.page_layout(ui),
|
||||
Page::Maps => self.page_maps(ui),
|
||||
Page::Video => self.page_video(ui),
|
||||
Page::Macros => self.page_macros(ui),
|
||||
Page::Macros => self.page_keybinds(ui),
|
||||
Page::Teams => self.page_teams(ui),
|
||||
});
|
||||
}
|
||||
@ -786,6 +797,7 @@ impl App {
|
||||
if ui.button("Add map").clicked() {
|
||||
self.profile.maps.push(Map {
|
||||
name: format!("map{}", self.profile.maps.len() + 1),
|
||||
category: "Mapped Keys".to_string(),
|
||||
hotkey: Hotkey("1".into()),
|
||||
hold: false,
|
||||
steps: vec![Step {
|
||||
@ -865,6 +877,7 @@ impl App {
|
||||
if ui.button("Add crop").clicked() {
|
||||
self.profile.video_fx.push(VideoFx {
|
||||
name: format!("crop{}", self.profile.video_fx.len() + 1),
|
||||
category: "Video FX".to_string(),
|
||||
enabled: true,
|
||||
source_slot: 2.min(self.profile.slots),
|
||||
source: NormRect {
|
||||
@ -1304,13 +1317,204 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
fn page_macros(&mut self, ui: &mut egui::Ui) {
|
||||
ui.heading("In-game macros");
|
||||
ui.label("Bind these on every account. enBoxer only sends keystrokes.");
|
||||
let text = print_macros(&self.profile);
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
ui.monospace(text);
|
||||
fn page_keybinds(&mut self, ui: &mut egui::Ui) {
|
||||
ui.heading("Key bindings + video FX");
|
||||
ui.label("Tree editor: click an item to edit it. Add as many or as few as you want per category.");
|
||||
ui.horizontal(|ui| {
|
||||
// LEFT: tree
|
||||
egui::SidePanel::left("tree")
|
||||
.resizable(true)
|
||||
.default_width(260.0)
|
||||
.show_inside(ui, |ui| {
|
||||
self.keybind_tree(ui);
|
||||
});
|
||||
// RIGHT: detail editor
|
||||
egui::CentralPanel::default().show_inside(ui, |ui| {
|
||||
self.keybind_detail(ui);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Item 2: tree (left pane). Categories as CollapsingHeaders,
|
||||
/// items as selectable buttons. Add/Remove buttons per
|
||||
/// category.
|
||||
fn keybind_tree(&mut self, ui: &mut egui::Ui) {
|
||||
let map_cats = ["Always On", "Combat", "Mapped Keys", "Character Sets", "Characters"];
|
||||
for cat in map_cats {
|
||||
let _header = egui::CollapsingHeader::new(cat)
|
||||
.default_open(cat == "Mapped Keys")
|
||||
.show(ui, |ui| {
|
||||
// List items in this category.
|
||||
let mut remove_idx = None;
|
||||
for (i, m) in self.profile.maps.iter().enumerate() {
|
||||
if m.category != cat {
|
||||
continue;
|
||||
}
|
||||
let selected = self.selected_node == Some(SelectedNode::Map(i));
|
||||
if ui.selectable_label(selected, format!("{} -> {}", m.name, m.hotkey.0)).clicked() {
|
||||
self.selected_node = Some(SelectedNode::Map(i));
|
||||
}
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("Remove").clicked() {
|
||||
remove_idx = Some(i);
|
||||
}
|
||||
});
|
||||
}
|
||||
if let Some(idx) = remove_idx {
|
||||
self.profile.maps.remove(idx);
|
||||
// Re-resolve selected_node so we don't
|
||||
// hold a stale index.
|
||||
match self.selected_node {
|
||||
Some(SelectedNode::Map(j)) if j == idx => self.selected_node = None,
|
||||
Some(SelectedNode::Map(j)) if j > idx => {
|
||||
self.selected_node = Some(SelectedNode::Map(j - 1));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if ui.button(format!("+ Add to {}", cat)).clicked() {
|
||||
let n = self.profile.maps.len();
|
||||
self.profile.maps.push(crate::profile::Map {
|
||||
category: cat.to_string(),
|
||||
name: format!("new_map_{}", n + 1),
|
||||
hotkey: crate::hotkey::Hotkey("".into()),
|
||||
hold: false,
|
||||
steps: Vec::new(),
|
||||
release_steps: Vec::new(),
|
||||
});
|
||||
self.selected_node = Some(SelectedNode::Map(n));
|
||||
}
|
||||
});
|
||||
}
|
||||
// Video FX branch.
|
||||
egui::CollapsingHeader::new("Video FX")
|
||||
.default_open(false)
|
||||
.show(ui, |ui| {
|
||||
let mut remove_idx = None;
|
||||
for (i, fx) in self.profile.video_fx.iter().enumerate() {
|
||||
let selected = self.selected_node == Some(SelectedNode::VideoFx(i));
|
||||
if ui.selectable_label(selected, fx.name.to_string()).clicked() {
|
||||
self.selected_node = Some(SelectedNode::VideoFx(i));
|
||||
}
|
||||
if ui.button("Remove").clicked() {
|
||||
remove_idx = Some(i);
|
||||
}
|
||||
}
|
||||
if let Some(idx) = remove_idx {
|
||||
self.profile.video_fx.remove(idx);
|
||||
match self.selected_node {
|
||||
Some(SelectedNode::VideoFx(j)) if j == idx => self.selected_node = None,
|
||||
Some(SelectedNode::VideoFx(j)) if j > idx => {
|
||||
self.selected_node = Some(SelectedNode::VideoFx(j - 1));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if ui.button("+ Add Video FX").clicked() {
|
||||
let n = self.profile.video_fx.len();
|
||||
self.profile.video_fx.push(crate::profile::VideoFx {
|
||||
category: "Video FX".to_string(),
|
||||
name: format!("new_fx_{}", n + 1),
|
||||
..Default::default()
|
||||
});
|
||||
self.selected_node = Some(SelectedNode::VideoFx(n));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Item 2: detail editor (right pane). Renders whichever
|
||||
/// item the user selected in the tree.
|
||||
fn keybind_detail(&mut self, ui: &mut egui::Ui) {
|
||||
match self.selected_node {
|
||||
Some(SelectedNode::Map(i)) => {
|
||||
if let Some(m) = self.profile.maps.get_mut(i) {
|
||||
ui.heading(format!("Map #{}", i + 1));
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("category");
|
||||
egui::ComboBox::from_label(format!("map_cat_{}", i))
|
||||
.selected_text(m.category.clone())
|
||||
.show_ui(ui, |ui| {
|
||||
for cat in ["Always On", "Combat", "Mapped Keys", "Character Sets", "Characters"] {
|
||||
ui.selectable_value(&mut m.category, cat.to_string(), cat);
|
||||
}
|
||||
});
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("name");
|
||||
ui.text_edit_singleline(&mut m.name);
|
||||
ui.label("hotkey");
|
||||
ui.text_edit_singleline(&mut m.hotkey.0);
|
||||
ui.checkbox(&mut m.hold, "hold");
|
||||
});
|
||||
ui.separator();
|
||||
ui.label("Steps (executed in order on press)");
|
||||
let mut step_remove = None;
|
||||
for (si, s) in m.steps.iter_mut().enumerate() {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(format!("{}", si + 1));
|
||||
ui.label("bind");
|
||||
let mut bind_str = s.bind.clone().unwrap_or_default();
|
||||
if ui.text_edit_singleline(&mut bind_str).changed() {
|
||||
s.bind = if bind_str.is_empty() { None } else { Some(bind_str) };
|
||||
}
|
||||
ui.label("key");
|
||||
let mut key_str = s.key.clone().unwrap_or_default();
|
||||
if ui.text_edit_singleline(&mut key_str).changed() {
|
||||
s.key = if key_str.is_empty() { None } else { Some(key_str) };
|
||||
}
|
||||
ui.label("target");
|
||||
ui.text_edit_singleline(&mut s.target);
|
||||
if ui.button("x").clicked() {
|
||||
step_remove = Some(si);
|
||||
}
|
||||
});
|
||||
}
|
||||
if let Some(si) = step_remove {
|
||||
m.steps.remove(si);
|
||||
}
|
||||
if ui.button("+ Add step").clicked() {
|
||||
m.steps.push(crate::profile::Step {
|
||||
bind: None,
|
||||
key: None,
|
||||
delay_ms: None,
|
||||
target: "all".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(SelectedNode::VideoFx(i)) => {
|
||||
if let Some(fx) = self.profile.video_fx.get_mut(i) {
|
||||
ui.heading(format!("Video FX #{}", i + 1));
|
||||
ui.horizontal(|ui| {
|
||||
ui.checkbox(&mut fx.enabled, "on");
|
||||
ui.label("name");
|
||||
ui.text_edit_singleline(&mut fx.name);
|
||||
ui.label("from slot");
|
||||
ui.add(egui::DragValue::new(&mut fx.source_slot).range(1..=16));
|
||||
ui.checkbox(&mut fx.pass_through, "clicks+keys through");
|
||||
});
|
||||
ui.label("source x y w h");
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(egui::DragValue::new(&mut fx.source.x));
|
||||
ui.add(egui::DragValue::new(&mut fx.source.y));
|
||||
ui.add(egui::DragValue::new(&mut fx.source.w));
|
||||
ui.add(egui::DragValue::new(&mut fx.source.h));
|
||||
});
|
||||
ui.label("viewer x y w h");
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(egui::DragValue::new(&mut fx.viewer.x));
|
||||
ui.add(egui::DragValue::new(&mut fx.viewer.y));
|
||||
ui.add(egui::DragValue::new(&mut fx.viewer.w));
|
||||
ui.add(egui::DragValue::new(&mut fx.viewer.h));
|
||||
});
|
||||
ui.label("fps");
|
||||
ui.add(egui::DragValue::new(&mut fx.fps).range(1..=60));
|
||||
}
|
||||
}
|
||||
None => {
|
||||
ui.label("Click an item on the left to edit it.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn page_teams(&mut self, ui: &mut egui::Ui) {
|
||||
|
||||
@ -179,6 +179,11 @@ pub struct Group {
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct Map {
|
||||
/// Item 2: tree category. One of: "Always On", "Combat",
|
||||
/// "Mapped Keys", "Character Sets", "Characters". Defaults
|
||||
/// to "Mapped Keys".
|
||||
#[serde(default = "default_map_category")]
|
||||
pub category: String,
|
||||
pub name: String,
|
||||
pub hotkey: Hotkey,
|
||||
#[serde(default)]
|
||||
@ -207,6 +212,9 @@ fn default_target() -> String {
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct VideoFx {
|
||||
/// Item 2: tree category. Always "Video FX" for now.
|
||||
#[serde(default = "default_vfx_category")]
|
||||
pub category: String,
|
||||
pub name: String,
|
||||
#[serde(default = "default_true")]
|
||||
pub enabled: bool,
|
||||
@ -324,6 +332,16 @@ fn default_one() -> f64 {
|
||||
1.0
|
||||
}
|
||||
|
||||
/// Item 2: default category for new Map entries.
|
||||
fn default_map_category() -> String {
|
||||
"Mapped Keys".to_string()
|
||||
}
|
||||
|
||||
/// Item 2: default category for new VideoFx entries.
|
||||
fn default_vfx_category() -> String {
|
||||
"Video FX".to_string()
|
||||
}
|
||||
|
||||
|
||||
impl NormRect {
|
||||
/// Values > 1 are pixels inside the window; otherwise fractions 0..=1 of the window.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user