From d478fa514c5d910b5cdeed26e0f15fea9b1d9cbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Mary=C5=84czak?= Date: Sat, 30 Dec 2023 19:46:55 +0100 Subject: [PATCH] Note Range Settings (#122) --- .../scene/menu_scene/iced_menu/settings.rs | 115 +++++++++++++- .../src/scene/menu_scene/iced_menu/theme.rs | 24 +++ neothesia/src/scene/menu_scene/mod.rs | 1 + .../scene/menu_scene/scroll_listener/mod.rs | 150 ++++++++++++++++++ 4 files changed, 282 insertions(+), 8 deletions(-) create mode 100644 neothesia/src/scene/menu_scene/scroll_listener/mod.rs diff --git a/neothesia/src/scene/menu_scene/iced_menu/settings.rs b/neothesia/src/scene/menu_scene/iced_menu/settings.rs index bd72a927..a677dd6c 100644 --- a/neothesia/src/scene/menu_scene/iced_menu/settings.rs +++ b/neothesia/src/scene/menu_scene/iced_menu/settings.rs @@ -5,7 +5,7 @@ use iced_core::{ Alignment, Length, Padding, }; use iced_runtime::Command; -use iced_widget::{column as col, container, pick_list, row, toggler}; +use iced_widget::{button, column as col, container, pick_list, row, toggler}; use crate::{ iced_utils::iced_state::Element, @@ -14,12 +14,18 @@ use crate::{ icons, layout::{BarLayout, Layout, PushIf}, neo_btn::NeoBtn, - preferences_group, + preferences_group, scroll_listener, }, target::Target, }; -use super::{centered_text, theme, top_padded, Data, InputDescriptor, Message}; +use super::{centered_text, theme, Data, InputDescriptor, Message}; + +#[derive(Debug, Clone)] +pub enum RangeUpdateKind { + Add, + Sub, +} #[derive(Debug, Clone)] pub enum SettingsMessage { @@ -29,6 +35,9 @@ pub enum SettingsMessage { OpenSoundFontPicker, SoundFontFileLoaded(Option), + + RangeStart(RangeUpdateKind), + RangeEnd(RangeUpdateKind), } impl From for Message { @@ -72,6 +81,28 @@ pub(super) fn update( } data.is_loading = false; } + SettingsMessage::RangeStart(kind) => match kind { + RangeUpdateKind::Add => { + let v = (target.config.piano_range().start() + 1).min(127); + if v + 24 < *target.config.piano_range().end() { + target.config.piano_range.0 = v; + } + } + RangeUpdateKind::Sub => { + target.config.piano_range.0 = target.config.piano_range.0.saturating_sub(1); + } + }, + SettingsMessage::RangeEnd(kind) => match kind { + RangeUpdateKind::Add => { + target.config.piano_range.1 = (target.config.piano_range.1 + 1).min(127); + } + RangeUpdateKind::Sub => { + let v = target.config.piano_range().end().saturating_sub(1); + if *target.config.piano_range().start() + 24 < v { + target.config.piano_range.1 = v; + } + } + }, } Command::none() @@ -139,6 +170,57 @@ fn input_group<'a>(data: &'a Data, _target: &Target) -> Element<'a, Message> { .build() } +fn counter<'a>( + value: u8, + msg: fn(RangeUpdateKind) -> SettingsMessage, +) -> Element<'a, SettingsMessage> { + let label = centered_text(value); + let sub = button(centered_text("-").width(30).height(30)) + .padding(0) + .style(theme::round_button()) + .on_press(msg(RangeUpdateKind::Sub)); + let add = button(centered_text("+").width(30).height(30)) + .padding(0) + .style(theme::round_button()) + .on_press(msg(RangeUpdateKind::Add)); + + let row = row![label, sub, add] + .spacing(10) + .align_items(Alignment::Center); + + scroll_listener::ScrollListener::new(row, move |delta| { + if delta.is_sign_positive() { + msg(RangeUpdateKind::Add) + } else { + msg(RangeUpdateKind::Sub) + } + }) + .into() +} + +fn note_range_group<'a>(_data: &'a Data, target: &Target) -> Element<'a, Message> { + let start = counter( + *target.config.piano_range().start(), + SettingsMessage::RangeStart, + ) + .map(Message::Settings); + let end = counter( + *target.config.piano_range().end(), + SettingsMessage::RangeEnd, + ) + .map(Message::Settings); + + preferences_group::PreferencesGroup::new() + .title("Note Range") + .push( + preferences_group::ActionRow::new() + .title("Start") + .suffix(start), + ) + .push(preferences_group::ActionRow::new().title("End").suffix(end)) + .build() +} + fn guidelines_group<'a>(_data: &'a Data, target: &Target) -> Element<'a, Message> { let toggler = toggler(None, target.config.vertical_guidelines, |v| { SettingsMessage::VerticalGuidelines(v).into() @@ -159,12 +241,18 @@ fn guidelines_group<'a>(_data: &'a Data, target: &Target) -> Element<'a, Message pub(super) fn view<'a>(data: &'a Data, target: &Target) -> Element<'a, Message> { let output_group = output_group(data, target); let input_group = input_group(data, target); + let note_range_group = note_range_group(data, target); let guidelines_group = guidelines_group(data, target); - let column = col![output_group, input_group, guidelines_group] - .spacing(10) - .width(Length::Fill) - .align_items(Alignment::Center); + let column = col![ + output_group, + input_group, + note_range_group, + guidelines_group + ] + .spacing(10) + .width(Length::Fill) + .align_items(Alignment::Center); let left = { let back = NeoBtn::new( @@ -194,8 +282,19 @@ pub(super) fn view<'a>(data: &'a Data, target: &Target) -> Element<'a, Message> left: 10.0, }); + let body = container(column).max_width(650).padding(Padding { + top: 50.0, + ..Padding::ZERO + }); + + let body = col![body] + .width(Length::Fill) + .align_items(Alignment::Center); + + let column = iced_widget::scrollable(body); + Layout::new() - .body(top_padded(column)) + .body(column) .bottom(BarLayout::new().left(left)) .into() } diff --git a/neothesia/src/scene/menu_scene/iced_menu/theme.rs b/neothesia/src/scene/menu_scene/iced_menu/theme.rs index d4b2f206..3827f610 100644 --- a/neothesia/src/scene/menu_scene/iced_menu/theme.rs +++ b/neothesia/src/scene/menu_scene/iced_menu/theme.rs @@ -97,6 +97,30 @@ impl iced_style::button::StyleSheet for ButtonStyle { } } +pub fn round_button() -> iced_style::theme::Button { + iced_style::theme::Button::Custom(Box::new(RoundButtonStyle)) +} + +struct RoundButtonStyle; + +impl iced_style::button::StyleSheet for RoundButtonStyle { + type Style = iced_style::Theme; + + fn active(&self, style: &Self::Style) -> button::Appearance { + button::Appearance { + border_radius: BorderRadius::from(f32::MAX), + ..ButtonStyle::active(&ButtonStyle, style) + } + } + + fn hovered(&self, style: &Self::Style) -> button::Appearance { + button::Appearance { + border_radius: BorderRadius::from(f32::MAX), + ..ButtonStyle::hovered(&ButtonStyle, style) + } + } +} + pub fn _checkbox() -> iced_style::theme::Checkbox { iced_style::theme::Checkbox::Custom(Box::new(CheckboxStyle)) } diff --git a/neothesia/src/scene/menu_scene/mod.rs b/neothesia/src/scene/menu_scene/mod.rs index 67f99fec..e2ef92e8 100644 --- a/neothesia/src/scene/menu_scene/mod.rs +++ b/neothesia/src/scene/menu_scene/mod.rs @@ -4,6 +4,7 @@ mod icons; mod layout; mod neo_btn; mod preferences_group; +mod scroll_listener; mod segment_button; mod track_card; mod wrap; diff --git a/neothesia/src/scene/menu_scene/scroll_listener/mod.rs b/neothesia/src/scene/menu_scene/scroll_listener/mod.rs new file mode 100644 index 00000000..2423926f --- /dev/null +++ b/neothesia/src/scene/menu_scene/scroll_listener/mod.rs @@ -0,0 +1,150 @@ +use iced_core::{mouse::ScrollDelta, Widget}; + +use crate::iced_utils::iced_state::Element; + +pub struct ScrollListener<'a, M> { + content: Element<'a, M>, + on_scroll: Box M>, +} + +impl<'a, M> ScrollListener<'a, M> { + pub fn new(content: impl Into>, on_scroll: impl Fn(f32) -> M + 'static) -> Self { + Self { + content: content.into(), + on_scroll: Box::new(on_scroll), + } + } +} + +impl<'a, M> Widget for ScrollListener<'a, M> { + fn width(&self) -> iced_core::Length { + self.content.as_widget().width() + } + + fn height(&self) -> iced_core::Length { + self.content.as_widget().height() + } + + fn layout( + &self, + tree: &mut iced_core::widget::Tree, + renderer: &super::Renderer, + limits: &iced_core::layout::Limits, + ) -> iced_core::layout::Node { + self.content.as_widget().layout(tree, renderer, limits) + } + + fn draw( + &self, + tree: &iced_core::widget::Tree, + renderer: &mut super::Renderer, + theme: &::Theme, + style: &iced_core::renderer::Style, + layout: iced_core::Layout<'_>, + cursor: iced_core::mouse::Cursor, + viewport: &iced_core::Rectangle, + ) { + self.content + .as_widget() + .draw(tree, renderer, theme, style, layout, cursor, viewport) + } + + fn tag(&self) -> iced_core::widget::tree::Tag { + self.content.as_widget().tag() + } + + fn state(&self) -> iced_core::widget::tree::State { + self.content.as_widget().state() + } + + fn children(&self) -> Vec { + self.content.as_widget().children() + } + + fn diff(&self, tree: &mut iced_core::widget::Tree) { + self.content.as_widget().diff(tree) + } + + fn operate( + &self, + state: &mut iced_core::widget::Tree, + layout: iced_core::Layout<'_>, + renderer: &super::Renderer, + operation: &mut dyn iced_core::widget::Operation, + ) { + self.content + .as_widget() + .operate(state, layout, renderer, operation) + } + + fn on_event( + &mut self, + state: &mut iced_core::widget::Tree, + event: iced_core::Event, + layout: iced_core::Layout<'_>, + cursor: iced_core::mouse::Cursor, + renderer: &super::Renderer, + clipboard: &mut dyn iced_core::Clipboard, + shell: &mut iced_core::Shell<'_, M>, + viewport: &iced_core::Rectangle, + ) -> iced_core::event::Status { + if let iced_core::event::Status::Captured = self.content.as_widget_mut().on_event( + state, + event.clone(), + layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ) { + return iced_core::event::Status::Captured; + } + + if let iced_core::Event::Mouse(iced_core::mouse::Event::WheelScrolled { delta }) = event { + let bounds = layout.bounds(); + + if cursor.is_over(bounds) { + let (ScrollDelta::Lines { y, .. } | ScrollDelta::Pixels { y, .. }) = delta; + + if y.abs() != 0.0 { + let msg = (self.on_scroll)(y); + shell.publish(msg); + return iced_core::event::Status::Captured; + } + } + } + + iced_core::event::Status::Ignored + } + + fn mouse_interaction( + &self, + state: &iced_core::widget::Tree, + layout: iced_core::Layout<'_>, + cursor: iced_core::mouse::Cursor, + viewport: &iced_core::Rectangle, + renderer: &super::Renderer, + ) -> iced_core::mouse::Interaction { + self.content + .as_widget() + .mouse_interaction(state, layout, cursor, viewport, renderer) + } + + fn overlay<'b>( + &'b mut self, + state: &'b mut iced_core::widget::Tree, + layout: iced_core::Layout<'_>, + renderer: &super::Renderer, + ) -> Option> { + self.content + .as_widget_mut() + .overlay(state, layout, renderer) + } +} + +impl<'a, M: 'a> From> for Element<'a, M> { + fn from(value: ScrollListener<'a, M>) -> Self { + Self::new(value) + } +}