fixed some bugs
This commit is contained in:
parent
97d2d6bd75
commit
464aad571a
|
|
@ -32,14 +32,14 @@
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.0.2</string>
|
<string>1.0.3</string>
|
||||||
<key>CFBundleSupportedPlatforms</key>
|
<key>CFBundleSupportedPlatforms</key>
|
||||||
<array>
|
<array>
|
||||||
<string>iPhoneOS</string>
|
<string>iPhoneOS</string>
|
||||||
<string>iPhoneSimulator</string>
|
<string>iPhoneSimulator</string>
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>1.0.2</string>
|
<string>1.0.3</string>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
<false/>
|
<false/>
|
||||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,15 @@ use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||||
use cpal::{BufferSize, SampleFormat, SampleRate, Stream, StreamConfig};
|
use cpal::{BufferSize, SampleFormat, SampleRate, Stream, StreamConfig};
|
||||||
#[cfg(all(not(target_os = "ios"), not(target_os = "android")))]
|
#[cfg(all(not(target_os = "ios"), not(target_os = "android")))]
|
||||||
use cpal::SupportedBufferSize;
|
use cpal::SupportedBufferSize;
|
||||||
use crossbeam_channel::{unbounded, Sender};
|
use crossbeam_channel::{unbounded, Receiver, Sender};
|
||||||
|
|
||||||
use crate::track::TrackData;
|
use crate::track::TrackData;
|
||||||
|
|
||||||
|
/// upstream notifications from the audio callback to the App tick loop.
|
||||||
|
pub enum EngineEvent {
|
||||||
|
TrackEnded { id: u64 },
|
||||||
|
}
|
||||||
|
|
||||||
/// lazy-initialises the engine clock's reference instant.
|
/// lazy-initialises the engine clock's reference instant.
|
||||||
fn engine_clock() -> Instant {
|
fn engine_clock() -> Instant {
|
||||||
static START: OnceLock<Instant> = OnceLock::new();
|
static START: OnceLock<Instant> = OnceLock::new();
|
||||||
|
|
@ -36,7 +41,7 @@ pub struct EngineState {
|
||||||
|
|
||||||
/// transport messages crossing from the ui thread into the cpal callback.
|
/// transport messages crossing from the ui thread into the cpal callback.
|
||||||
enum Cmd {
|
enum Cmd {
|
||||||
Load(Arc<TrackData>),
|
Load { td: Arc<TrackData>, id: u64 },
|
||||||
Play,
|
Play,
|
||||||
Pause,
|
Pause,
|
||||||
Stop,
|
Stop,
|
||||||
|
|
@ -47,6 +52,7 @@ enum Cmd {
|
||||||
pub struct AudioEngine {
|
pub struct AudioEngine {
|
||||||
pub state: Arc<EngineState>,
|
pub state: Arc<EngineState>,
|
||||||
cmd_tx: Sender<Cmd>,
|
cmd_tx: Sender<Cmd>,
|
||||||
|
event_rx: Receiver<EngineEvent>,
|
||||||
#[cfg(not(target_os = "android"))]
|
#[cfg(not(target_os = "android"))]
|
||||||
_stream: Stream,
|
_stream: Stream,
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
|
|
@ -132,9 +138,12 @@ impl AudioEngine {
|
||||||
last_callback_nanos: AtomicU64::new(0),
|
last_callback_nanos: AtomicU64::new(0),
|
||||||
});
|
});
|
||||||
let (cmd_tx, cmd_rx) = unbounded::<Cmd>();
|
let (cmd_tx, cmd_rx) = unbounded::<Cmd>();
|
||||||
|
let (event_tx, event_rx) = unbounded::<EngineEvent>();
|
||||||
|
|
||||||
let cb_state = state.clone();
|
let cb_state = state.clone();
|
||||||
let mut current: Option<Arc<TrackData>> = None;
|
let mut current: Option<Arc<TrackData>> = None;
|
||||||
|
let mut current_id: u64 = 0;
|
||||||
|
let mut emitted_end: bool = false;
|
||||||
let mut local_pos: u64 = 0;
|
let mut local_pos: u64 = 0;
|
||||||
let mut local_playing = false;
|
let mut local_playing = false;
|
||||||
|
|
||||||
|
|
@ -146,11 +155,13 @@ impl AudioEngine {
|
||||||
move |out: &mut [f32], _info: &cpal::OutputCallbackInfo| {
|
move |out: &mut [f32], _info: &cpal::OutputCallbackInfo| {
|
||||||
while let Ok(cmd) = cmd_rx.try_recv() {
|
while let Ok(cmd) = cmd_rx.try_recv() {
|
||||||
match cmd {
|
match cmd {
|
||||||
Cmd::Load(td) => {
|
Cmd::Load { td, id } => {
|
||||||
cb_state
|
cb_state
|
||||||
.total_frames
|
.total_frames
|
||||||
.store(td.total_samples() as u64, Ordering::Release);
|
.store(td.total_samples() as u64, Ordering::Release);
|
||||||
current = Some(td);
|
current = Some(td);
|
||||||
|
current_id = id;
|
||||||
|
emitted_end = false;
|
||||||
local_pos = 0;
|
local_pos = 0;
|
||||||
cb_state.frame_pos.store(0, Ordering::Release);
|
cb_state.frame_pos.store(0, Ordering::Release);
|
||||||
cb_state.last_callback_pos.store(0, Ordering::Release);
|
cb_state.last_callback_pos.store(0, Ordering::Release);
|
||||||
|
|
@ -213,6 +224,10 @@ impl AudioEngine {
|
||||||
for f in 0..frames_out {
|
for f in 0..frames_out {
|
||||||
let dst = f * device_ch;
|
let dst = f * device_ch;
|
||||||
if local_pos >= total {
|
if local_pos >= total {
|
||||||
|
if !emitted_end {
|
||||||
|
let _ = event_tx.send(EngineEvent::TrackEnded { id: current_id });
|
||||||
|
emitted_end = true;
|
||||||
|
}
|
||||||
for c in 0..device_ch {
|
for c in 0..device_ch {
|
||||||
out[dst + c] = 0.0;
|
out[dst + c] = 0.0;
|
||||||
}
|
}
|
||||||
|
|
@ -259,6 +274,7 @@ impl AudioEngine {
|
||||||
Ok(AudioEngine {
|
Ok(AudioEngine {
|
||||||
state,
|
state,
|
||||||
cmd_tx,
|
cmd_tx,
|
||||||
|
event_rx,
|
||||||
_stream: stream,
|
_stream: stream,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -286,9 +302,12 @@ impl AudioEngine {
|
||||||
last_callback_nanos: AtomicU64::new(0),
|
last_callback_nanos: AtomicU64::new(0),
|
||||||
});
|
});
|
||||||
let (cmd_tx, cmd_rx) = unbounded::<Cmd>();
|
let (cmd_tx, cmd_rx) = unbounded::<Cmd>();
|
||||||
|
let (event_tx, event_rx) = unbounded::<EngineEvent>();
|
||||||
|
|
||||||
let cb_state = state.clone();
|
let cb_state = state.clone();
|
||||||
let mut current: Option<Arc<TrackData>> = None;
|
let mut current: Option<Arc<TrackData>> = None;
|
||||||
|
let mut current_id: u64 = 0;
|
||||||
|
let mut emitted_end: bool = false;
|
||||||
let mut local_pos: u64 = 0;
|
let mut local_pos: u64 = 0;
|
||||||
let mut local_playing = false;
|
let mut local_playing = false;
|
||||||
|
|
||||||
|
|
@ -299,11 +318,13 @@ impl AudioEngine {
|
||||||
move |stream: &AudioStream, audio_data: *mut c_void, num_frames: i32| {
|
move |stream: &AudioStream, audio_data: *mut c_void, num_frames: i32| {
|
||||||
while let Ok(cmd) = cmd_rx.try_recv() {
|
while let Ok(cmd) = cmd_rx.try_recv() {
|
||||||
match cmd {
|
match cmd {
|
||||||
Cmd::Load(td) => {
|
Cmd::Load { td, id } => {
|
||||||
cb_state
|
cb_state
|
||||||
.total_frames
|
.total_frames
|
||||||
.store(td.total_samples() as u64, Ordering::Release);
|
.store(td.total_samples() as u64, Ordering::Release);
|
||||||
current = Some(td);
|
current = Some(td);
|
||||||
|
current_id = id;
|
||||||
|
emitted_end = false;
|
||||||
local_pos = 0;
|
local_pos = 0;
|
||||||
cb_state.frame_pos.store(0, Ordering::Release);
|
cb_state.frame_pos.store(0, Ordering::Release);
|
||||||
cb_state.last_callback_pos.store(0, Ordering::Release);
|
cb_state.last_callback_pos.store(0, Ordering::Release);
|
||||||
|
|
@ -382,6 +403,10 @@ impl AudioEngine {
|
||||||
for f in 0..frames {
|
for f in 0..frames {
|
||||||
let dst = f * device_ch;
|
let dst = f * device_ch;
|
||||||
if local_pos >= total {
|
if local_pos >= total {
|
||||||
|
if !emitted_end {
|
||||||
|
let _ = event_tx.send(EngineEvent::TrackEnded { id: current_id });
|
||||||
|
emitted_end = true;
|
||||||
|
}
|
||||||
for c in 0..device_ch {
|
for c in 0..device_ch {
|
||||||
out[dst + c] = 0.0;
|
out[dst + c] = 0.0;
|
||||||
}
|
}
|
||||||
|
|
@ -451,6 +476,7 @@ impl AudioEngine {
|
||||||
Ok(AudioEngine {
|
Ok(AudioEngine {
|
||||||
state,
|
state,
|
||||||
cmd_tx,
|
cmd_tx,
|
||||||
|
event_rx,
|
||||||
_stream: stream,
|
_stream: stream,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -460,9 +486,14 @@ impl AudioEngine {
|
||||||
self.state.sample_rate.load(Ordering::Acquire)
|
self.state.sample_rate.load(Ordering::Acquire)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// hands incoming pcm to the audio callback and rewinds the playhead to zero.
|
/// hands incoming pcm to the audio callback and rewinds the playhead to zero, tagged with a load id matched against TrackEnded events.
|
||||||
pub fn load(&self, track: Arc<TrackData>) {
|
pub fn load(&self, track: Arc<TrackData>, id: u64) {
|
||||||
let _ = self.cmd_tx.send(Cmd::Load(track));
|
let _ = self.cmd_tx.send(Cmd::Load { td: track, id });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// drains pending engine-to-app events.
|
||||||
|
pub fn drain_events(&self) -> Vec<EngineEvent> {
|
||||||
|
self.event_rx.try_iter().collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// resumes output from the current frame position.
|
/// resumes output from the current frame position.
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ use iced_widget::scrollable::AbsoluteOffset;
|
||||||
|
|
||||||
use crate::analyzer::FrameData;
|
use crate::analyzer::FrameData;
|
||||||
use crate::analyzer_worker::AnalyzerWorker;
|
use crate::analyzer_worker::AnalyzerWorker;
|
||||||
use crate::engine::AudioEngine;
|
use crate::engine::{AudioEngine, EngineEvent};
|
||||||
use crate::library::{self, Track};
|
use crate::library::{self, Track};
|
||||||
use crate::library_worker::{LibraryUpdate, LibraryWorker};
|
use crate::library_worker::{LibraryUpdate, LibraryWorker};
|
||||||
|
|
||||||
|
|
@ -41,13 +41,10 @@ pub struct App {
|
||||||
/// modal status message shown over the UI while waiting on iOS file-coordinator caching.
|
/// modal status message shown over the UI while waiting on iOS file-coordinator caching.
|
||||||
pub coordinating_message: Option<String>,
|
pub coordinating_message: Option<String>,
|
||||||
|
|
||||||
/// sidebar scroll offset mirrored from the on_scroll callback.
|
|
||||||
pub sidebar_scroll: AbsoluteOffset,
|
|
||||||
|
|
||||||
/// settings panel scroll offset mirrored from the on_scroll callback.
|
/// settings panel scroll offset mirrored from the on_scroll callback.
|
||||||
pub settings_scroll: AbsoluteOffset,
|
pub settings_scroll: AbsoluteOffset,
|
||||||
|
|
||||||
/// pending sidebar scroll restore against the live scrollable widget.
|
/// pending sidebar snap to the currently playing track row.
|
||||||
pub restore_sidebar_scroll: bool,
|
pub restore_sidebar_scroll: bool,
|
||||||
|
|
||||||
/// pending settings scroll restore against the live scrollable widget.
|
/// pending settings scroll restore against the live scrollable widget.
|
||||||
|
|
@ -86,7 +83,7 @@ impl Default for Settings {
|
||||||
glass: true,
|
glass: true,
|
||||||
entropy_on: false,
|
entropy_on: false,
|
||||||
album_colors: false,
|
album_colors: false,
|
||||||
mirrored: false,
|
mirrored: true,
|
||||||
inverted: false,
|
inverted: false,
|
||||||
entropy_strength: 0.0,
|
entropy_strength: 0.0,
|
||||||
hue: 0.9,
|
hue: 0.9,
|
||||||
|
|
@ -124,7 +121,7 @@ pub enum Message {
|
||||||
ToggleImmersive,
|
ToggleImmersive,
|
||||||
ToggleChrome,
|
ToggleChrome,
|
||||||
ToggleSettings,
|
ToggleSettings,
|
||||||
SidebarScrolled(AbsoluteOffset),
|
NoOp,
|
||||||
SettingsScrolled(AbsoluteOffset),
|
SettingsScrolled(AbsoluteOffset),
|
||||||
SetGlass(bool),
|
SetGlass(bool),
|
||||||
SetEntropy(bool),
|
SetEntropy(bool),
|
||||||
|
|
@ -177,7 +174,6 @@ impl App {
|
||||||
track_loading: false,
|
track_loading: false,
|
||||||
library_progress: None,
|
library_progress: None,
|
||||||
coordinating_message: None,
|
coordinating_message: None,
|
||||||
sidebar_scroll: AbsoluteOffset::default(),
|
|
||||||
settings_scroll: AbsoluteOffset::default(),
|
settings_scroll: AbsoluteOffset::default(),
|
||||||
restore_sidebar_scroll: false,
|
restore_sidebar_scroll: false,
|
||||||
restore_settings_scroll: false,
|
restore_settings_scroll: false,
|
||||||
|
|
@ -198,7 +194,7 @@ impl App {
|
||||||
|
|
||||||
/// checks whether a logical-coords point falls inside the scrollable sidebar region.
|
/// checks whether a logical-coords point falls inside the scrollable sidebar region.
|
||||||
pub fn point_in_sidebar(&self, x: f32, y: f32, viewport_height: f32) -> bool {
|
pub fn point_in_sidebar(&self, x: f32, y: f32, viewport_height: f32) -> bool {
|
||||||
if self.immersive || self.show_settings {
|
if self.immersive {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
x >= 0.0
|
x >= 0.0
|
||||||
|
|
@ -360,7 +356,6 @@ impl App {
|
||||||
|
|
||||||
/// drains worker updates, advances tracks, and refreshes analyzer frames each frame.
|
/// drains worker updates, advances tracks, and refreshes analyzer frames each frame.
|
||||||
pub fn tick(&mut self) {
|
pub fn tick(&mut self) {
|
||||||
let mut needs_resort = false;
|
|
||||||
for upd in self.library_worker.drain_updates() {
|
for upd in self.library_worker.drain_updates() {
|
||||||
match upd {
|
match upd {
|
||||||
LibraryUpdate::Meta {
|
LibraryUpdate::Meta {
|
||||||
|
|
@ -382,10 +377,7 @@ impl App {
|
||||||
t.album = album;
|
t.album = album;
|
||||||
}
|
}
|
||||||
if let Some(tn) = track_number {
|
if let Some(tn) = track_number {
|
||||||
if t.track_number != Some(tn) {
|
|
||||||
t.track_number = Some(tn);
|
t.track_number = Some(tn);
|
||||||
needs_resort = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -437,7 +429,7 @@ impl App {
|
||||||
match result {
|
match result {
|
||||||
Ok(td) => {
|
Ok(td) => {
|
||||||
if let Some(eng) = &self.engine {
|
if let Some(eng) = &self.engine {
|
||||||
eng.load(td.clone());
|
eng.load(td.clone(), self.current_decode_id);
|
||||||
if self.playing {
|
if self.playing {
|
||||||
eng.play();
|
eng.play();
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -452,17 +444,37 @@ impl App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if needs_resort {
|
self.handle_engine_events();
|
||||||
self.resort_library();
|
|
||||||
}
|
|
||||||
|
|
||||||
self.advance_if_finished();
|
|
||||||
|
|
||||||
self.worker.publish_playhead(self.position());
|
self.worker.publish_playhead(self.position());
|
||||||
self.frame_data = self.worker.latest_frames();
|
self.frame_data = self.worker.latest_frames();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// drains engine end-of-track notifications and advances when the id matches the current load.
|
||||||
|
fn handle_engine_events(&mut self) {
|
||||||
|
let Some(eng) = self.engine.as_ref() else { return };
|
||||||
|
for ev in eng.drain_events() {
|
||||||
|
match ev {
|
||||||
|
EngineEvent::TrackEnded { id } => {
|
||||||
|
if id == self.current_decode_id && self.playing && !self.track_loading {
|
||||||
|
self.advance_to_next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// wraps the playhead onto the next track in the current sidebar order.
|
||||||
|
fn advance_to_next(&mut self) {
|
||||||
|
let Some(idx) = self.selected_track else { return };
|
||||||
|
if self.library.tracks.is_empty() { return; }
|
||||||
|
let next = (idx + 1) % self.library.tracks.len();
|
||||||
|
self.selected_track = Some(next);
|
||||||
|
self.load_index(next);
|
||||||
|
}
|
||||||
|
|
||||||
/// re-sorts the library on arrival of track-number tags and preserves the current selection.
|
/// re-sorts the library on arrival of track-number tags and preserves the current selection.
|
||||||
|
#[allow(dead_code)]
|
||||||
fn resort_library(&mut self) {
|
fn resort_library(&mut self) {
|
||||||
let selected_path = self
|
let selected_path = self
|
||||||
.selected_track
|
.selected_track
|
||||||
|
|
@ -474,27 +486,6 @@ impl App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// rolls onto the next track once the playhead reaches the end, pausing past the final entry.
|
|
||||||
fn advance_if_finished(&mut self) {
|
|
||||||
if !self.playing {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let Some(idx) = self.selected_track else { return };
|
|
||||||
if self.position() < 0.999 {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let next = idx + 1;
|
|
||||||
if next >= self.library.tracks.len() {
|
|
||||||
self.playing = false;
|
|
||||||
if let Some(eng) = &self.engine {
|
|
||||||
eng.pause();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
self.selected_track = Some(next);
|
|
||||||
self.load_index(next);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// dispatches a UI message into the matching state mutation and worker call.
|
/// dispatches a UI message into the matching state mutation and worker call.
|
||||||
pub fn update(&mut self, msg: Message) {
|
pub fn update(&mut self, msg: Message) {
|
||||||
match msg {
|
match msg {
|
||||||
|
|
@ -541,6 +532,7 @@ impl App {
|
||||||
Message::SelectTrack(idx) => {
|
Message::SelectTrack(idx) => {
|
||||||
if idx < self.library.tracks.len() {
|
if idx < self.library.tracks.len() {
|
||||||
self.selected_track = Some(idx);
|
self.selected_track = Some(idx);
|
||||||
|
self.playing = true;
|
||||||
self.load_index(idx);
|
self.load_index(idx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -596,7 +588,7 @@ impl App {
|
||||||
self.restore_settings_scroll = true;
|
self.restore_settings_scroll = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::SidebarScrolled(off) => self.sidebar_scroll = off,
|
Message::NoOp => {}
|
||||||
Message::SettingsScrolled(off) => self.settings_scroll = off,
|
Message::SettingsScrolled(off) => self.settings_scroll = off,
|
||||||
Message::SetGlass(on) => self.settings.glass = on,
|
Message::SetGlass(on) => self.settings.glass = on,
|
||||||
Message::SetEntropy(on) => self.settings.entropy_on = on,
|
Message::SetEntropy(on) => self.settings.entropy_on = on,
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,9 @@ use super::theme::palette;
|
||||||
pub const SIDEBAR_W: f32 = 280.0;
|
pub const SIDEBAR_W: f32 = 280.0;
|
||||||
pub const TOP_BAR_H: f32 = 44.0;
|
pub const TOP_BAR_H: f32 = 44.0;
|
||||||
pub const TRANSPORT_H: f32 = 72.0;
|
pub const TRANSPORT_H: f32 = 72.0;
|
||||||
const ROW_H: f32 = 56.0;
|
pub const ROW_H: f32 = 56.0;
|
||||||
|
pub const ROW_SPACING: f32 = 2.0;
|
||||||
|
pub const SIDEBAR_PADDING_TOP: f32 = 8.0;
|
||||||
const THUMB: f32 = 40.0;
|
const THUMB: f32 = 40.0;
|
||||||
|
|
||||||
/// stable id of the sidebar scrollable.
|
/// stable id of the sidebar scrollable.
|
||||||
|
|
@ -180,14 +182,13 @@ fn sidebar(app: &App) -> Element<'_, Message, Theme, iced_wgpu::Renderer> {
|
||||||
let selected = app.selected_track;
|
let selected = app.selected_track;
|
||||||
|
|
||||||
let inner = lazy(key, move |_| {
|
let inner = lazy(key, move |_| {
|
||||||
let mut col = column![].spacing(2).padding(Padding::from([8, 8]));
|
let mut col = column![]
|
||||||
|
.spacing(ROW_SPACING)
|
||||||
|
.padding(Padding::from([SIDEBAR_PADDING_TOP as u16, 8]));
|
||||||
for (i, t) in tracks.iter().enumerate() {
|
for (i, t) in tracks.iter().enumerate() {
|
||||||
col = col.push(track_row_owned(i, t.clone(), selected == Some(i)));
|
col = col.push(track_row_owned(i, t.clone(), selected == Some(i)));
|
||||||
}
|
}
|
||||||
scrollable(col)
|
scrollable(col).id(sidebar_scroll_id()).height(Length::Fill)
|
||||||
.id(sidebar_scroll_id())
|
|
||||||
.on_scroll(|vp| Message::SidebarScrolled(vp.absolute_offset()))
|
|
||||||
.height(Length::Fill)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
container(inner)
|
container(inner)
|
||||||
|
|
@ -253,17 +254,11 @@ fn track_row_owned(
|
||||||
.padding(0)
|
.padding(0)
|
||||||
.on_press(Message::SelectTrack(idx))
|
.on_press(Message::SelectTrack(idx))
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.style(move |_t: &Theme, status: ButtonStatus| {
|
.style(move |_t: &Theme, _status: ButtonStatus| {
|
||||||
let bg = match (active, status) {
|
let bg = if active {
|
||||||
(true, _) => Some(Background::Color(Color {
|
Some(Background::Color(Color { a: 0.20, ..palette::playing() }))
|
||||||
a: 0.20,
|
} else {
|
||||||
..palette::accent()
|
None
|
||||||
})),
|
|
||||||
(false, ButtonStatus::Hovered) => Some(Background::Color(Color {
|
|
||||||
a: 0.06,
|
|
||||||
..palette::accent()
|
|
||||||
})),
|
|
||||||
_ => None,
|
|
||||||
};
|
};
|
||||||
button::Style {
|
button::Style {
|
||||||
background: bg,
|
background: bg,
|
||||||
|
|
@ -524,10 +519,13 @@ fn settings_overlay(app: &App) -> Element<'_, Message, Theme, iced_wgpu::Rendere
|
||||||
.align_x(iced_wgpu::core::alignment::Horizontal::Right)
|
.align_x(iced_wgpu::core::alignment::Horizontal::Right)
|
||||||
.align_y(iced_wgpu::core::alignment::Vertical::Center);
|
.align_y(iced_wgpu::core::alignment::Vertical::Center);
|
||||||
|
|
||||||
let panel = container(stack![scroll, close])
|
let panel = mouse_area(
|
||||||
|
container(stack![scroll, close])
|
||||||
.width(Length::Fixed(SETTINGS_W))
|
.width(Length::Fixed(SETTINGS_W))
|
||||||
.height(Length::Fill)
|
.height(Length::Fill)
|
||||||
.style(settings_panel_style);
|
.style(settings_panel_style)
|
||||||
|
)
|
||||||
|
.on_press(Message::NoOp);
|
||||||
|
|
||||||
container(panel)
|
container(panel)
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ pub mod palette {
|
||||||
const TEXT_C: Color = Color::from_rgb(0.92, 0.92, 0.95);
|
const TEXT_C: Color = Color::from_rgb(0.92, 0.92, 0.95);
|
||||||
const TEXT_DIM_C: Color = Color::from_rgb(0.55, 0.55, 0.62);
|
const TEXT_DIM_C: Color = Color::from_rgb(0.55, 0.55, 0.62);
|
||||||
const ACCENT_C: Color = Color::from_rgb(0.78, 0.62, 0.95);
|
const ACCENT_C: Color = Color::from_rgb(0.78, 0.62, 0.95);
|
||||||
|
const PLAYING_C: Color = Color::from_rgb(0.40, 0.70, 1.00);
|
||||||
|
|
||||||
pub fn bg() -> Color { BG_C }
|
pub fn bg() -> Color { BG_C }
|
||||||
pub fn sidebar() -> Color { SIDEBAR_C }
|
pub fn sidebar() -> Color { SIDEBAR_C }
|
||||||
|
|
@ -17,6 +18,7 @@ pub mod palette {
|
||||||
pub fn text() -> Color { TEXT_C }
|
pub fn text() -> Color { TEXT_C }
|
||||||
pub fn text_dim() -> Color { TEXT_DIM_C }
|
pub fn text_dim() -> Color { TEXT_DIM_C }
|
||||||
pub fn accent() -> Color { ACCENT_C }
|
pub fn accent() -> Color { ACCENT_C }
|
||||||
|
pub fn playing() -> Color { PLAYING_C }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// supplies the clear color handed to the wgpu compositor each frame.
|
/// supplies the clear color handed to the wgpu compositor each frame.
|
||||||
|
|
|
||||||
|
|
@ -206,17 +206,17 @@ impl ViewportHandle {
|
||||||
let logical = self.viewport.logical_size();
|
let logical = self.viewport.logical_size();
|
||||||
let h = logical.height;
|
let h = logical.height;
|
||||||
let w = logical.width;
|
let w = logical.width;
|
||||||
self.touch_in_sidebar = self.state.point_in_sidebar(x, y, h);
|
|
||||||
let in_settings = self.state.point_in_settings(x, y, w);
|
let in_settings = self.state.point_in_settings(x, y, w);
|
||||||
|
self.touch_in_sidebar = !in_settings && self.state.point_in_sidebar(x, y, h);
|
||||||
self.touch_start = Some((x, y));
|
self.touch_start = Some((x, y));
|
||||||
self.touch_drift = 0.0;
|
self.touch_drift = 0.0;
|
||||||
self.last_touch = Some((x, y));
|
self.last_touch = Some((x, y));
|
||||||
if self.touch_in_sidebar {
|
if in_settings {
|
||||||
self.push_mouse_move(x, y);
|
|
||||||
self.settings_touch = SettingsTouch::None;
|
|
||||||
} else if in_settings {
|
|
||||||
self.settings_touch = SettingsTouch::Pending;
|
self.settings_touch = SettingsTouch::Pending;
|
||||||
self.push_mouse_move(x, y);
|
self.push_mouse_move(x, y);
|
||||||
|
} else if self.touch_in_sidebar {
|
||||||
|
self.push_mouse_move(x, y);
|
||||||
|
self.settings_touch = SettingsTouch::None;
|
||||||
} else {
|
} else {
|
||||||
self.settings_touch = SettingsTouch::None;
|
self.settings_touch = SettingsTouch::None;
|
||||||
self.push_mouse_button(x, y, 0, true);
|
self.push_mouse_button(x, y, 0, true);
|
||||||
|
|
@ -245,11 +245,9 @@ impl ViewportHandle {
|
||||||
match self.settings_touch {
|
match self.settings_touch {
|
||||||
SettingsTouch::Scroll => {
|
SettingsTouch::Scroll => {
|
||||||
if dy.abs() > 0.0 {
|
if dy.abs() > 0.0 {
|
||||||
let p = Point::new(x, y);
|
|
||||||
self.events.push(Event::Mouse(mouse::Event::WheelScrolled {
|
self.events.push(Event::Mouse(mouse::Event::WheelScrolled {
|
||||||
delta: mouse::ScrollDelta::Pixels { x: 0.0, y: dy },
|
delta: mouse::ScrollDelta::Pixels { x: 0.0, y: dy },
|
||||||
}));
|
}));
|
||||||
self.cursor = mouse::Cursor::Available(p);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SettingsTouch::Drag => {
|
SettingsTouch::Drag => {
|
||||||
|
|
@ -257,13 +255,14 @@ impl ViewportHandle {
|
||||||
}
|
}
|
||||||
SettingsTouch::Pending => {}
|
SettingsTouch::Pending => {}
|
||||||
SettingsTouch::None => {
|
SettingsTouch::None => {
|
||||||
self.push_mouse_move(x, y);
|
if self.touch_in_sidebar {
|
||||||
if self.touch_in_sidebar && dy.abs() > 0.0 {
|
if dy.abs() > 0.0 {
|
||||||
let p = Point::new(x, y);
|
|
||||||
self.events.push(Event::Mouse(mouse::Event::WheelScrolled {
|
self.events.push(Event::Mouse(mouse::Event::WheelScrolled {
|
||||||
delta: mouse::ScrollDelta::Pixels { x: 0.0, y: dy },
|
delta: mouse::ScrollDelta::Pixels { x: 0.0, y: dy },
|
||||||
}));
|
}));
|
||||||
self.cursor = mouse::Cursor::Available(p);
|
}
|
||||||
|
} else {
|
||||||
|
self.push_mouse_move(x, y);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -597,13 +596,24 @@ fn render(handle: &mut ViewportHandle) {
|
||||||
|
|
||||||
/// drains scroll-restore flags from App state into a side-channel struct.
|
/// drains scroll-restore flags from App state into a side-channel struct.
|
||||||
fn take_pending_scroll_restores(state: &mut App) -> ScrollRestore {
|
fn take_pending_scroll_restores(state: &mut App) -> ScrollRestore {
|
||||||
let r = ScrollRestore {
|
let sidebar = if state.restore_sidebar_scroll {
|
||||||
sidebar: state.restore_sidebar_scroll.then_some(state.sidebar_scroll),
|
Some(sidebar_snap_offset(state.selected_track))
|
||||||
settings: state.restore_settings_scroll.then_some(state.settings_scroll),
|
} else {
|
||||||
|
None
|
||||||
};
|
};
|
||||||
|
let settings = state.restore_settings_scroll.then_some(state.settings_scroll);
|
||||||
state.restore_sidebar_scroll = false;
|
state.restore_sidebar_scroll = false;
|
||||||
state.restore_settings_scroll = false;
|
state.restore_settings_scroll = false;
|
||||||
r
|
ScrollRestore { sidebar, settings }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// vertical offset placing the playing row at the top of the sidebar viewport.
|
||||||
|
fn sidebar_snap_offset(selected: Option<usize>) -> AbsoluteOffset {
|
||||||
|
let idx = selected.unwrap_or(0) as f32;
|
||||||
|
AbsoluteOffset {
|
||||||
|
x: 0.0,
|
||||||
|
y: player::SIDEBAR_PADDING_TOP + idx * (player::ROW_H + player::ROW_SPACING),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// pushes the stored sidebar and settings scroll offsets onto the UserInterface.
|
/// pushes the stored sidebar and settings scroll offsets onto the UserInterface.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue