From 3e21dfeb7faf7b48f6b0e4a103ae3b4ae2bcdb4e Mon Sep 17 00:00:00 2001 From: Benjamin Klum Date: Sat, 19 Oct 2024 22:31:10 +0200 Subject: [PATCH] #1256 Docs: Context-sensitive help depending on mouse cursor position --- Cargo.lock | 16 +- main/src/infrastructure/ui/mapping_panel.rs | 316 ++++++++++-------- main/src/infrastructure/ui/mod.rs | 2 + .../infrastructure/ui/ui_element_container.rs | 32 ++ swell-ui/src/units.rs | 44 ++- swell-ui/src/window.rs | 67 +++- 6 files changed, 309 insertions(+), 168 deletions(-) create mode 100644 main/src/infrastructure/ui/ui_element_container.rs diff --git a/Cargo.lock b/Cargo.lock index 51421ced7..de43e68a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5379,7 +5379,7 @@ dependencies = [ [[package]] name = "reaper-common-types" version = "0.1.0" -source = "git+https://github.com/helgoboss/reaper-rs.git?branch=master#480118626fb5707e51e809d7fc647cf9dce115d2" +source = "git+https://github.com/helgoboss/reaper-rs.git?branch=master#4b3d4264a5474ae5fffb7ba8a8f4c5f908744973" dependencies = [ "hex-literal", "nutype", @@ -5390,7 +5390,7 @@ dependencies = [ [[package]] name = "reaper-fluent" version = "0.1.0" -source = "git+https://github.com/helgoboss/reaper-rs.git?branch=master#480118626fb5707e51e809d7fc647cf9dce115d2" +source = "git+https://github.com/helgoboss/reaper-rs.git?branch=master#4b3d4264a5474ae5fffb7ba8a8f4c5f908744973" dependencies = [ "fragile", "reaper-low", @@ -5401,7 +5401,7 @@ dependencies = [ [[package]] name = "reaper-high" version = "0.1.0" -source = "git+https://github.com/helgoboss/reaper-rs.git?branch=master#480118626fb5707e51e809d7fc647cf9dce115d2" +source = "git+https://github.com/helgoboss/reaper-rs.git?branch=master#4b3d4264a5474ae5fffb7ba8a8f4c5f908744973" dependencies = [ "backtrace", "base64 0.13.0", @@ -5430,7 +5430,7 @@ dependencies = [ [[package]] name = "reaper-low" version = "0.1.0" -source = "git+https://github.com/helgoboss/reaper-rs.git?branch=master#480118626fb5707e51e809d7fc647cf9dce115d2" +source = "git+https://github.com/helgoboss/reaper-rs.git?branch=master#4b3d4264a5474ae5fffb7ba8a8f4c5f908744973" dependencies = [ "c_str_macro", "cc 1.0.83", @@ -5444,7 +5444,7 @@ dependencies = [ [[package]] name = "reaper-macros" version = "0.1.0" -source = "git+https://github.com/helgoboss/reaper-rs.git?branch=master#480118626fb5707e51e809d7fc647cf9dce115d2" +source = "git+https://github.com/helgoboss/reaper-rs.git?branch=master#4b3d4264a5474ae5fffb7ba8a8f4c5f908744973" dependencies = [ "darling 0.10.2", "quote", @@ -5454,7 +5454,7 @@ dependencies = [ [[package]] name = "reaper-medium" version = "0.1.0" -source = "git+https://github.com/helgoboss/reaper-rs.git?branch=master#480118626fb5707e51e809d7fc647cf9dce115d2" +source = "git+https://github.com/helgoboss/reaper-rs.git?branch=master#4b3d4264a5474ae5fffb7ba8a8f4c5f908744973" dependencies = [ "c_str_macro", "camino", @@ -5475,7 +5475,7 @@ dependencies = [ [[package]] name = "reaper-rx" version = "0.1.0" -source = "git+https://github.com/helgoboss/reaper-rs.git?branch=master#480118626fb5707e51e809d7fc647cf9dce115d2" +source = "git+https://github.com/helgoboss/reaper-rs.git?branch=master#4b3d4264a5474ae5fffb7ba8a8f4c5f908744973" dependencies = [ "crossbeam-channel", "helgoboss-midi", @@ -5729,7 +5729,7 @@ checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f" [[package]] name = "rppxml-parser" version = "0.1.0" -source = "git+https://github.com/helgoboss/reaper-rs.git?branch=master#480118626fb5707e51e809d7fc647cf9dce115d2" +source = "git+https://github.com/helgoboss/reaper-rs.git?branch=master#4b3d4264a5474ae5fffb7ba8a8f4c5f908744973" dependencies = [ "splitty", ] diff --git a/main/src/infrastructure/ui/mapping_panel.rs b/main/src/infrastructure/ui/mapping_panel.rs index 7fd4bad1d..83b1f4361 100644 --- a/main/src/infrastructure/ui/mapping_panel.rs +++ b/main/src/infrastructure/ui/mapping_panel.rs @@ -36,8 +36,8 @@ use helgobox_api::persistence::{ TrackToolAction, VirtualControlElementCharacter, }; use swell_ui::{ - DeviceContext, DialogUnits, Point, SharedView, SwellStringArg, View, ViewContext, WeakView, - Window, + DeviceContext, DialogUnits, Pixels, Point, SharedView, SwellStringArg, View, ViewContext, + WeakView, Window, }; use crate::application::{ @@ -74,6 +74,7 @@ use crate::infrastructure::plugin::BackboneShell; use crate::infrastructure::ui::bindings::root; use crate::infrastructure::ui::color_panel::{ColorPanel, ColorPanelDesc}; use crate::infrastructure::ui::menus::{get_midi_input_device_list_label, get_param_name}; +use crate::infrastructure::ui::ui_element_container::{UiElement, UiElementContainer}; use crate::infrastructure::ui::util::colors::ColorPair; use crate::infrastructure::ui::util::{ close_child_panel_if_open, colors, compartment_parameter_dropdown_contents, @@ -106,6 +107,7 @@ pub struct MappingPanel { extra_panel: RefCell>>, last_touched_mode_parameter: RefCell>>, last_touched_source_character: RefCell>>, + ui_element_container: RefCell, // Fires when a mapping is about to change or the panel is hidden. party_is_over_subject: RefCell>, } @@ -164,6 +166,7 @@ impl MappingPanel { extra_panel: Default::default(), last_touched_mode_parameter: Default::default(), last_touched_source_character: Default::default(), + ui_element_container: Default::default(), party_is_over_subject: Default::default(), } } @@ -3896,6 +3899,89 @@ impl<'a> MutableMappingPanel<'a> { } } +const SOURCE_MIN_MAX_ELEMENTS: &[u32] = &[ + root::ID_SETTINGS_SOURCE_LABEL, + #[cfg(not(target_os = "macos"))] + root::ID_SETTINGS_SOURCE_GROUP, + root::ID_SETTINGS_SOURCE_MIN_LABEL, + root::ID_SETTINGS_SOURCE_MAX_LABEL, + root::ID_SETTINGS_MIN_SOURCE_VALUE_EDIT_CONTROL, + root::ID_SETTINGS_MIN_SOURCE_VALUE_SLIDER_CONTROL, + root::ID_SETTINGS_MAX_SOURCE_VALUE_EDIT_CONTROL, + root::ID_SETTINGS_MAX_SOURCE_VALUE_SLIDER_CONTROL, +]; +const OUT_OF_RANGE_ELEMENTS: &[u32] = &[ + root::ID_MODE_OUT_OF_RANGE_LABEL_TEXT, + root::ID_MODE_OUT_OF_RANGE_COMBOX_BOX, +]; +const GROUP_INTERACTION_ELEMENTS: &[u32] = &[ + root::ID_MODE_GROUP_INTERACTION_LABEL_TEXT, + root::ID_MODE_GROUP_INTERACTION_COMBO_BOX, +]; +const VALUE_SEQUENCE_ELEMENTS: &[u32] = &[ + root::ID_SETTINGS_TARGET_SEQUENCE_LABEL_TEXT, + root::ID_MODE_TARGET_SEQUENCE_EDIT_CONTROL, +]; +const TARGET_MIN_MAX_ELEMENTS: &[u32] = &[ + root::ID_SETTINGS_TARGET_LABEL_TEXT, + #[cfg(not(target_os = "macos"))] + root::ID_SETTINGS_TARGET_GROUP, + root::ID_SETTINGS_MIN_TARGET_LABEL_TEXT, + root::ID_SETTINGS_MIN_TARGET_VALUE_SLIDER_CONTROL, + root::ID_SETTINGS_MIN_TARGET_VALUE_EDIT_CONTROL, + root::ID_SETTINGS_MIN_TARGET_VALUE_TEXT, + root::ID_SETTINGS_MAX_TARGET_LABEL_TEXT, + root::ID_SETTINGS_MAX_TARGET_VALUE_SLIDER_CONTROL, + root::ID_SETTINGS_MAX_TARGET_VALUE_EDIT_CONTROL, + root::ID_SETTINGS_MAX_TARGET_VALUE_TEXT, +]; +const FEEDBACK_TYPE_ELEMENTS: &[u32] = &[root::IDC_MODE_FEEDBACK_TYPE_COMBO_BOX]; +const ROUND_TARGET_VALUE_ELEMENTS: &[u32] = &[root::ID_SETTINGS_ROUND_TARGET_VALUE_CHECK_BOX]; +const TAKEOVER_MODE_ELEMENTS: &[u32] = &[root::ID_MODE_TAKEOVER_LABEL, root::ID_MODE_TAKEOVER_MODE]; +const CONTROL_TRANSFORMATION_ELEMENTS: &[u32] = &[ + root::ID_MODE_EEL_CONTROL_TRANSFORMATION_LABEL, + root::ID_MODE_EEL_CONTROL_TRANSFORMATION_EDIT_CONTROL, + root::ID_MODE_EEL_CONTROL_TRANSFORMATION_DETAIL_BUTTON, +]; +const ABSOLUTE_MODE_ELEMENTS: &[u32] = &[ + root::ID_SETTINGS_MODE_COMBO_BOX, + root::ID_SETTINGS_MODE_LABEL, +]; +const STEP_MIN_ELEMENTS: &[u32] = &[ + root::ID_SETTINGS_MIN_STEP_SIZE_LABEL_TEXT, + #[cfg(not(target_os = "macos"))] + root::ID_SETTINGS_STEP_SIZE_GROUP, + root::ID_SETTINGS_MIN_STEP_SIZE_SLIDER_CONTROL, + root::ID_SETTINGS_MIN_STEP_SIZE_EDIT_CONTROL, + root::ID_SETTINGS_MIN_STEP_SIZE_VALUE_TEXT, +]; +const STEP_MAX_ELEMENTS: &[u32] = &[ + root::ID_SETTINGS_MAX_STEP_SIZE_LABEL_TEXT, + root::ID_SETTINGS_MAX_STEP_SIZE_SLIDER_CONTROL, + root::ID_SETTINGS_MAX_STEP_SIZE_EDIT_CONTROL, + root::ID_SETTINGS_MAX_STEP_SIZE_VALUE_TEXT, +]; +const MAKE_ABSOLUTE_ELEMENTS: &[u32] = &[root::ID_SETTINGS_MAKE_ABSOLUTE_CHECK_BOX]; +const RELATIVE_FILTER_ELEMENTS: &[u32] = &[root::ID_MODE_RELATIVE_FILTER_COMBO_BOX]; +const FIRE_MODE_ELEMENTS: &[u32] = &[ + root::ID_MODE_FIRE_COMBO_BOX, + root::ID_MODE_FIRE_LINE_2_LABEL_1, + root::ID_MODE_FIRE_LINE_2_SLIDER_CONTROL, + root::ID_MODE_FIRE_LINE_2_EDIT_CONTROL, + root::ID_MODE_FIRE_LINE_2_LABEL_2, + root::ID_MODE_FIRE_LINE_3_LABEL_1, + root::ID_MODE_FIRE_LINE_3_SLIDER_CONTROL, + root::ID_MODE_FIRE_LINE_3_EDIT_CONTROL, + root::ID_MODE_FIRE_LINE_3_LABEL_2, +]; +const REVERSE_ELEMENTS: &[u32] = &[root::ID_SETTINGS_REVERSE_CHECK_BOX]; +const FEEDBACK_TRANSFORMATION_ELEMENTS: &[u32] = &[ + root::ID_MODE_EEL_FEEDBACK_TRANSFORMATION_EDIT_CONTROL, + root::IDC_MODE_FEEDBACK_TYPE_BUTTON, +]; +const ROTATE_ELEMENTS: &[u32] = &[root::ID_SETTINGS_ROTATE_CHECK_BOX]; +const BUTTON_FILTER_ELEMENTS: &[u32] = &[root::ID_MODE_BUTTON_FILTER_COMBO_BOX]; + impl<'a> ImmutableMappingPanel<'a> { // For hitting target with on/off or -/+ depending on target character. fn hit_target_special(&self, state: bool) -> Result<(), &'static str> { @@ -4146,7 +4232,7 @@ impl<'a> ImmutableMappingPanel<'a> { fn invalidate_source_control_visibilities(&self) { let source = self.source; // Show/hide stuff - self.show_if( + self.enable_if( source.supports_type(), &[ root::ID_SOURCE_TYPE_LABEL_TEXT, @@ -4155,15 +4241,13 @@ impl<'a> ImmutableMappingPanel<'a> { ); } - fn show_if(&self, condition: bool, control_resource_ids: &[u32]) { - for id in control_resource_ids { - self.view.require_control(*id).set_visible(condition); - } - } - fn enable_if(&self, condition: bool, control_resource_ids: &[u32]) { for id in control_resource_ids { if let Some(control) = self.view.require_window().find_control(*id) { + self.panel + .ui_element_container + .borrow_mut() + .set_visible(*id, condition); control.set_visible(condition); } } @@ -6160,7 +6244,7 @@ impl<'a> ImmutableMappingPanel<'a> { } else { (Some("Target inactive!"), false, false, None, false) }; - self.show_if( + self.enable_if( read_enabled, &[ root::ID_TARGET_VALUE_LABEL_TEXT, @@ -6402,38 +6486,13 @@ impl<'a> ImmutableMappingPanel<'a> { // For all source characters { let show_source_min_max = is_relevant(ModeParameter::SourceMinMax); - self.enable_if( - show_source_min_max, - &[ - root::ID_SETTINGS_SOURCE_LABEL, - #[cfg(not(target_os = "macos"))] - root::ID_SETTINGS_SOURCE_GROUP, - root::ID_SETTINGS_SOURCE_MIN_LABEL, - root::ID_SETTINGS_SOURCE_MAX_LABEL, - root::ID_SETTINGS_MIN_SOURCE_VALUE_EDIT_CONTROL, - root::ID_SETTINGS_MIN_SOURCE_VALUE_SLIDER_CONTROL, - root::ID_SETTINGS_MAX_SOURCE_VALUE_EDIT_CONTROL, - root::ID_SETTINGS_MAX_SOURCE_VALUE_SLIDER_CONTROL, - ], - ); + self.enable_if(show_source_min_max, SOURCE_MIN_MAX_ELEMENTS); let show_reverse = is_relevant(ModeParameter::Reverse); - self.enable_if(show_reverse, &[root::ID_SETTINGS_REVERSE_CHECK_BOX]); + self.enable_if(show_reverse, REVERSE_ELEMENTS); let show_out_of_range_behavior = is_relevant(ModeParameter::OutOfRangeBehavior); - self.enable_if( - show_out_of_range_behavior, - &[ - root::ID_MODE_OUT_OF_RANGE_LABEL_TEXT, - root::ID_MODE_OUT_OF_RANGE_COMBOX_BOX, - ], - ); + self.enable_if(show_out_of_range_behavior, OUT_OF_RANGE_ELEMENTS); let show_group_interaction = is_relevant(ModeParameter::GroupInteraction); - self.enable_if( - show_group_interaction, - &[ - root::ID_MODE_GROUP_INTERACTION_LABEL_TEXT, - root::ID_MODE_GROUP_INTERACTION_COMBO_BOX, - ], - ); + self.enable_if(show_group_interaction, GROUP_INTERACTION_ELEMENTS); let target_controls_make_sense = if target_wants_relative_control && (!feedback_is_on || !target_can_report_current_value) { @@ -6444,78 +6503,32 @@ impl<'a> ImmutableMappingPanel<'a> { let show_target_value_sequence = target_controls_make_sense && is_relevant(ModeParameter::TargetValueSequence) && real_target.is_some(); - self.enable_if( - show_target_value_sequence, - &[ - root::ID_SETTINGS_TARGET_SEQUENCE_LABEL_TEXT, - root::ID_MODE_TARGET_SEQUENCE_EDIT_CONTROL, - ], - ); + self.enable_if(show_target_value_sequence, VALUE_SEQUENCE_ELEMENTS); let show_target_min_max = target_controls_make_sense && is_relevant(ModeParameter::TargetMinMax) && real_target.is_some(); - self.enable_if( - show_target_min_max, - &[ - root::ID_SETTINGS_TARGET_LABEL_TEXT, - #[cfg(not(target_os = "macos"))] - root::ID_SETTINGS_TARGET_GROUP, - root::ID_SETTINGS_MIN_TARGET_LABEL_TEXT, - root::ID_SETTINGS_MIN_TARGET_VALUE_SLIDER_CONTROL, - root::ID_SETTINGS_MIN_TARGET_VALUE_EDIT_CONTROL, - root::ID_SETTINGS_MIN_TARGET_VALUE_TEXT, - root::ID_SETTINGS_MAX_TARGET_LABEL_TEXT, - root::ID_SETTINGS_MAX_TARGET_VALUE_SLIDER_CONTROL, - root::ID_SETTINGS_MAX_TARGET_VALUE_EDIT_CONTROL, - root::ID_SETTINGS_MAX_TARGET_VALUE_TEXT, - ], - ); + self.enable_if(show_target_min_max, TARGET_MIN_MAX_ELEMENTS); let show_feedback_transformation = is_relevant(ModeParameter::FeedbackTransformation) || is_relevant(ModeParameter::TextualFeedbackExpression); self.enable_if( show_feedback_transformation, - &[ - root::ID_MODE_EEL_FEEDBACK_TRANSFORMATION_EDIT_CONTROL, - root::IDC_MODE_FEEDBACK_TYPE_BUTTON, - ], + FEEDBACK_TRANSFORMATION_ELEMENTS, ); let show_feedback_type = is_relevant(ModeParameter::FeedbackType); - self.enable_if( - show_feedback_type, - &[root::IDC_MODE_FEEDBACK_TYPE_COMBO_BOX], - ); + self.enable_if(show_feedback_type, FEEDBACK_TYPE_ELEMENTS); } // For knobs/faders and buttons { let show_round_controls = is_relevant(ModeParameter::RoundTargetValue) && self.target_with_context().is_known_to_be_roundable(); - self.enable_if( - show_round_controls, - &[root::ID_SETTINGS_ROUND_TARGET_VALUE_CHECK_BOX], - ); + self.enable_if(show_round_controls, ROUND_TARGET_VALUE_ELEMENTS); let show_takeover = target_can_report_current_value && is_relevant(ModeParameter::TakeoverMode); - self.enable_if( - show_takeover, - &[root::ID_MODE_TAKEOVER_LABEL, root::ID_MODE_TAKEOVER_MODE], - ); + self.enable_if(show_takeover, TAKEOVER_MODE_ELEMENTS); let show_control_transformation = is_relevant(ModeParameter::ControlTransformation); - self.enable_if( - show_control_transformation, - &[ - root::ID_MODE_EEL_CONTROL_TRANSFORMATION_LABEL, - root::ID_MODE_EEL_CONTROL_TRANSFORMATION_EDIT_CONTROL, - root::ID_MODE_EEL_CONTROL_TRANSFORMATION_DETAIL_BUTTON, - ], - ); + self.enable_if(show_control_transformation, CONTROL_TRANSFORMATION_ELEMENTS); let show_absolute_mode = is_relevant(ModeParameter::AbsoluteMode); - self.enable_if( - show_absolute_mode, - &[ - root::ID_SETTINGS_MODE_COMBO_BOX, - root::ID_SETTINGS_MODE_LABEL, - ], - ); + self.enable_if(show_absolute_mode, ABSOLUTE_MODE_ELEMENTS); self.enable_if( show_round_controls || show_takeover @@ -6536,38 +6549,14 @@ impl<'a> ImmutableMappingPanel<'a> { step_min_is_relevant || step_max_is_relevant, &[root::ID_SETTINGS_STEP_SIZE_LABEL_TEXT], ); - self.enable_if( - step_min_is_relevant, - &[ - root::ID_SETTINGS_MIN_STEP_SIZE_LABEL_TEXT, - #[cfg(not(target_os = "macos"))] - root::ID_SETTINGS_STEP_SIZE_GROUP, - root::ID_SETTINGS_MIN_STEP_SIZE_SLIDER_CONTROL, - root::ID_SETTINGS_MIN_STEP_SIZE_EDIT_CONTROL, - root::ID_SETTINGS_MIN_STEP_SIZE_VALUE_TEXT, - ], - ); - self.enable_if( - step_max_is_relevant, - &[ - root::ID_SETTINGS_MAX_STEP_SIZE_LABEL_TEXT, - root::ID_SETTINGS_MAX_STEP_SIZE_SLIDER_CONTROL, - root::ID_SETTINGS_MAX_STEP_SIZE_EDIT_CONTROL, - root::ID_SETTINGS_MAX_STEP_SIZE_VALUE_TEXT, - ], - ); + self.enable_if(step_min_is_relevant, STEP_MIN_ELEMENTS); + self.enable_if(step_max_is_relevant, STEP_MAX_ELEMENTS); let show_rotate = is_relevant(ModeParameter::Rotate); - self.enable_if(show_rotate, &[root::ID_SETTINGS_ROTATE_CHECK_BOX]); + self.enable_if(show_rotate, ROTATE_ELEMENTS); let show_make_absolute = is_relevant(ModeParameter::MakeAbsolute); - self.enable_if( - show_make_absolute, - &[root::ID_SETTINGS_MAKE_ABSOLUTE_CHECK_BOX], - ); + self.enable_if(show_make_absolute, MAKE_ABSOLUTE_ELEMENTS); let show_relative_filter = is_relevant(ModeParameter::RelativeFilter); - self.enable_if( - show_relative_filter, - &[root::ID_MODE_RELATIVE_FILTER_COMBO_BOX], - ); + self.enable_if(show_relative_filter, RELATIVE_FILTER_ELEMENTS); self.enable_if( step_min_is_relevant || step_max_is_relevant @@ -6580,22 +6569,9 @@ impl<'a> ImmutableMappingPanel<'a> { // For buttons { let show_button_filter = is_relevant(ModeParameter::ButtonFilter); - self.enable_if(show_button_filter, &[root::ID_MODE_BUTTON_FILTER_COMBO_BOX]); + self.enable_if(show_button_filter, BUTTON_FILTER_ELEMENTS); let show_fire_mode = is_relevant(ModeParameter::FireMode); - self.enable_if( - show_fire_mode, - &[ - root::ID_MODE_FIRE_COMBO_BOX, - root::ID_MODE_FIRE_LINE_2_LABEL_1, - root::ID_MODE_FIRE_LINE_2_SLIDER_CONTROL, - root::ID_MODE_FIRE_LINE_2_EDIT_CONTROL, - root::ID_MODE_FIRE_LINE_2_LABEL_2, - root::ID_MODE_FIRE_LINE_3_LABEL_1, - root::ID_MODE_FIRE_LINE_3_SLIDER_CONTROL, - root::ID_MODE_FIRE_LINE_3_EDIT_CONTROL, - root::ID_MODE_FIRE_LINE_3_LABEL_2, - ], - ); + self.enable_if(show_fire_mode, FIRE_MODE_ELEMENTS); self.enable_if( show_button_filter || show_fire_mode, &[root::ID_MODE_BUTTON_GROUP_BOX], @@ -6749,7 +6725,7 @@ impl<'a> ImmutableMappingPanel<'a> { initiator, ); } - self.show_if( + self.enable_if( label.is_some(), &[ root::ID_MODE_FIRE_LINE_2_SLIDER_CONTROL, @@ -6791,7 +6767,7 @@ impl<'a> ImmutableMappingPanel<'a> { initiator, ); } - self.show_if( + self.enable_if( option.is_some(), &[ root::ID_MODE_FIRE_LINE_3_SLIDER_CONTROL, @@ -7138,6 +7114,15 @@ impl View for MappingPanel { self.glue_color_panel.clone().open(window); self.help_color_panel.clone().open(window); } + let mut container = self.ui_element_container.borrow_mut(); + for child in window.children() { + let element = UiElement { + id: child.resource_id(), + rect: window.screen_to_client(&child.window_rect()), + visible: child.is_visible(), + }; + container.add_element(element); + } true } @@ -7186,6 +7171,7 @@ impl View for MappingPanel { fn on_destroy(self: SharedView, _window: Window) { self.window_cache.replace(None); + self.ui_element_container.replace(Default::default()); } fn button_clicked(self: SharedView, resource_id: u32) { @@ -7490,10 +7476,18 @@ impl View for MappingPanel { } } - // fn mouse_moved(self: SharedView, position: Point) -> bool { - // // dbg!(position); - // false - // } + fn mouse_moved(self: SharedView, position: Point) -> bool { + let container = self.ui_element_container.borrow(); + let mut resource_ids = + container.hit_test(Point::new(position.x.get() as _, position.y.get() as _)); + let mode_param = resource_ids + .find(|id| *id != 0) + .and_then(find_mode_parameter_associated_with_resource); + self.last_touched_mode_parameter + .borrow_mut() + .set(mode_param); + false + } } const SOURCE_MATCH_INDICATOR_TIMER_ID: usize = 570; @@ -8547,3 +8541,39 @@ impl Section { } } } + +fn find_mode_parameter_associated_with_resource(id: u32) -> Option { + use ModeParameter::*; + const RESOURCES_TO_MODE_PARAM: &[(&[u32], ModeParameter)] = &[ + (SOURCE_MIN_MAX_ELEMENTS, SourceMinMax), + (REVERSE_ELEMENTS, Reverse), + (OUT_OF_RANGE_ELEMENTS, OutOfRangeBehavior), + (TAKEOVER_MODE_ELEMENTS, TakeoverMode), + (CONTROL_TRANSFORMATION_ELEMENTS, ControlTransformation), + (VALUE_SEQUENCE_ELEMENTS, TargetValueSequence), + (TARGET_MIN_MAX_ELEMENTS, TargetMinMax), + (STEP_MIN_ELEMENTS, StepSizeMin), + (STEP_MAX_ELEMENTS, StepSizeMax), + (RELATIVE_FILTER_ELEMENTS, RelativeFilter), + (FIRE_MODE_ELEMENTS, FireMode), + (MAKE_ABSOLUTE_ELEMENTS, MakeAbsolute), + (FEEDBACK_TYPE_ELEMENTS, FeedbackType), + (ROUND_TARGET_VALUE_ELEMENTS, RoundTargetValue), + (ABSOLUTE_MODE_ELEMENTS, AbsoluteMode), + (GROUP_INTERACTION_ELEMENTS, GroupInteraction), + (FEEDBACK_TRANSFORMATION_ELEMENTS, FeedbackTransformation), + (ROTATE_ELEMENTS, Rotate), + (BUTTON_FILTER_ELEMENTS, ButtonFilter), + ]; + // 0 => TextualFeedbackExpression, + // 0 => StepFactorMin, + // 0 => StepFactorMax, + RESOURCES_TO_MODE_PARAM + .iter() + .find_map(|(elements, param)| { + if !elements.contains(&id) { + return None; + } + Some(*param) + }) +} diff --git a/main/src/infrastructure/ui/mod.rs b/main/src/infrastructure/ui/mod.rs index 6a57d7cb9..8a57276de 100644 --- a/main/src/infrastructure/ui/mod.rs +++ b/main/src/infrastructure/ui/mod.rs @@ -85,3 +85,5 @@ pub mod menus; pub mod color_panel; pub mod instance_panel; pub mod welcome_panel; + +mod ui_element_container; diff --git a/main/src/infrastructure/ui/ui_element_container.rs b/main/src/infrastructure/ui/ui_element_container.rs new file mode 100644 index 000000000..e1f125b99 --- /dev/null +++ b/main/src/infrastructure/ui/ui_element_container.rs @@ -0,0 +1,32 @@ +use swell_ui::{Point, Rect}; + +#[derive(Debug, Default)] +pub struct UiElementContainer { + elements: Vec, +} + +#[derive(Debug)] +pub struct UiElement { + pub id: u32, + pub rect: Rect, + pub visible: bool, +} + +impl UiElementContainer { + pub fn add_element(&mut self, element: UiElement) { + self.elements.push(element); + } + + pub fn set_visible(&mut self, id: u32, visible: bool) { + if let Some(el) = self.elements.iter_mut().find(|e| e.id == id) { + el.visible = visible; + } + } + + pub fn hit_test(&self, point: Point) -> impl Iterator + '_ { + self.elements + .iter() + .filter(move |e| e.visible && e.rect.contains(point)) + .map(|e| e.id) + } +} diff --git a/swell-ui/src/units.rs b/swell-ui/src/units.rs index 863a4d96f..6954f17b7 100644 --- a/swell-ui/src/units.rs +++ b/swell-ui/src/units.rs @@ -235,20 +235,52 @@ pub struct DialogScaling { pub height_scale: f64, } +#[derive(Copy, Clone, Debug)] pub struct Rect { - pub x: i32, - pub y: i32, + pub left: i32, + pub top: i32, pub width: u32, pub height: u32, } +impl Rect { + pub fn contains(&self, point: Point) -> bool { + point.x >= self.left + && point.y >= self.top + && point.x < self.right() + && point.y < self.bottom() + } + + pub fn right(&self) -> i32 { + self.left + self.width as i32 + } + + pub fn bottom(&self) -> i32 { + self.top + self.height as i32 + } + + // pub fn normalize(&self, parent_height: u32) -> Self { + // #[cfg(target_os = "macos")] + // { + // Self { + // top: parent_height as i32 - self.top, + // ..*self + // } + // } + // #[cfg(not(target_os = "macos"))] + // { + // *self + // } + // } +} + impl From for Rect { fn from(value: RECT) -> Self { Self { - x: value.left, - y: value.top, - width: (value.right - value.left) as _, - height: (value.bottom - value.top) as _, + left: value.left, + top: value.top, + width: (value.right - value.left).abs() as _, + height: (value.bottom - value.top).abs() as _, } } } diff --git a/swell-ui/src/window.rs b/swell-ui/src/window.rs index 8449398c3..b02c91bf5 100644 --- a/swell-ui/src/window.rs +++ b/swell-ui/src/window.rs @@ -37,6 +37,11 @@ impl Window { Self::new(window_ptr as _).ok_or("couldn't obtain window from raw window handle") } + /// Attention: On macOS, this returns the first subview, not the first child window. + pub fn next_child(prev_child: Window) -> Option { + get_related_window(prev_child, raw::GW_HWNDNEXT) + } + pub fn screen_size() -> Dimensions { Dimensions::new(Self::screen_width(), Self::screen_height()) } @@ -113,18 +118,40 @@ impl Window { )); } + pub fn screen_to_client(&self, rect: &RECT) -> Rect { + let mut top_left = raw::POINT { + x: rect.left, + y: rect.top, + }; + let mut bottom_right = raw::POINT { + x: rect.right, + y: rect.bottom, + }; + unsafe { + let swell = Swell::get(); + swell.ScreenToClient(self.raw, &mut top_left as *mut _); + swell.ScreenToClient(self.raw, &mut bottom_right as *mut _); + } + Rect { + left: top_left.x, + top: top_left.y, + width: (bottom_right.x - top_left.x) as _, + height: (bottom_right.y - top_left.y) as _, + } + } + pub fn client_size(self) -> Dimensions { let rect = self.client_rect(); Dimensions::new( - Pixels(rect.right as u32 - rect.left as u32), - Pixels(rect.bottom as u32 - rect.top as u32), + Pixels(rect.right() as u32 - rect.left as u32), + Pixels(rect.bottom() as u32 - rect.top as u32), ) } - pub fn client_rect(self) -> RECT { + pub fn client_rect(self) -> Rect { let mut rect = RECT::default(); unsafe { Swell::get().GetClientRect(self.raw, &mut rect) }; - rect + rect.into() } pub fn raw_hwnd(self) -> Hwnd { @@ -269,10 +296,8 @@ impl Window { } /// Attention: On macOS, this returns the first subview, not the first child window. - pub fn first_child(&self) -> Option { - let swell = Swell::get(); - let ptr = unsafe { swell.GetWindow(self.raw, raw::GW_CHILD as _) }; - Window::new(ptr) + pub fn first_child(self) -> Option { + get_related_window(self, raw::GW_CHILD) } #[cfg(target_os = "linux")] @@ -633,6 +658,19 @@ impl Window { } } + pub fn children(self) -> impl Iterator { + let mut child = self.first_child(); + child.into_iter().chain( + (0..) + .map(move |_| { + child = Window::next_child(child?); + child + }) + .take_while(|w| w.is_some()) + .map(|w| w.unwrap()), + ) + } + pub fn resize_first_child_according_to_parent(self) { unsafe { extern "C" fn resize_proc(arg1: raw::HWND, _arg2: raw::LPARAM) -> raw::BOOL { @@ -770,12 +808,13 @@ impl Window { } } - pub fn window_rect(self) -> Rect { + /// Uses the OS coordinate system. On macOS, y == 0 is at the bottom of the screen! + pub fn window_rect(self) -> raw::RECT { let mut rect = RECT::default(); unsafe { Swell::get().GetWindowRect(self.raw, &mut rect as *mut _); } - rect.into() + rect } pub fn move_to_pixels(self, point: Point) { @@ -950,7 +989,7 @@ impl XBridgeWindow { /// Usually an empty window. pub fn create(parent_window: Window) -> Result { let mut x_window_id: core::ffi::c_ulong = 0; - let rect = parent_window.client_rect(); + let rect: raw::RECT = parent_window.client_rect().into(); let x_bridge_hwnd = unsafe { Swell::get().SWELL_CreateXBridgeWindow( parent_window.raw(), @@ -1047,3 +1086,9 @@ impl ZOrder { } } } + +fn get_related_window(window: Window, relation: u32) -> Option { + let swell = Swell::get(); + let ptr = unsafe { swell.GetWindow(window.raw, relation as _) }; + Window::new(ptr) +}