654 lines
24 KiB
Rust
654 lines
24 KiB
Rust
use cosmic::iced::border::Shape as BorderShape;
|
|
use cosmic::iced::{Alignment, Length, Rectangle, mouse};
|
|
use cosmic::widget::{button, column, container, image, mouse_area, row, scrollable, slider};
|
|
use cosmic::{Element, theme};
|
|
use ferese_ipc::media::Playback;
|
|
use ferese_theme::icons;
|
|
use serde_json::json;
|
|
|
|
use super::{Menu, MenuRows, MenuStyle};
|
|
use crate::{
|
|
BarMetrics, FereseShell, Message, ShellTheme, accented_icon, bar_content, color, color_with_opacity, motion,
|
|
};
|
|
|
|
const SEEK_THUMB_DIAMETER: f32 = 10.0;
|
|
|
|
fn seek_slider_style(style: MenuStyle) -> cosmic::theme::iced::Slider {
|
|
let paint = std::rc::Rc::new(move |_: &cosmic::Theme| {
|
|
use cosmic::iced::widget::slider::{Breakpoint, Handle, HandleShape, Rail, Style};
|
|
|
|
let accent = color_with_opacity(style.theme.accent, style.opacity);
|
|
Style {
|
|
rail: Rail {
|
|
backgrounds: (accent.into(), style.muted.scale_alpha(0.10).into()),
|
|
width: 4.0,
|
|
border: cosmic::iced::Border {
|
|
shape: BorderShape::Circular,
|
|
radius: 2.0.into(),
|
|
..Default::default()
|
|
},
|
|
},
|
|
handle: Handle {
|
|
corner_shape: BorderShape::Circular,
|
|
shape: HandleShape::Circle {
|
|
radius: SEEK_THUMB_DIAMETER / 2.0,
|
|
},
|
|
background: accent.into(),
|
|
border_width: 0.0,
|
|
border_color: cosmic::iced::Color::TRANSPARENT,
|
|
},
|
|
breakpoint: Breakpoint {
|
|
color: cosmic::iced::Color::TRANSPARENT,
|
|
},
|
|
}
|
|
});
|
|
|
|
cosmic::theme::iced::Slider::Custom {
|
|
active: paint.clone(),
|
|
hovered: paint.clone(),
|
|
dragging: paint,
|
|
}
|
|
}
|
|
|
|
fn seek_pill<'a>(
|
|
content: impl Into<Element<'a, cosmic::Action<Message>>>,
|
|
style: MenuStyle,
|
|
) -> Element<'a, cosmic::Action<Message>> {
|
|
container(content)
|
|
.width(Length::Fill)
|
|
.padding([2, 0])
|
|
.class(theme::Container::custom(move |_| container::Style {
|
|
background: Some(color_with_opacity(style.theme.border, style.opacity).into()),
|
|
border: cosmic::iced::Border {
|
|
shape: BorderShape::Circular,
|
|
radius: 12.0.into(),
|
|
..Default::default()
|
|
},
|
|
..Default::default()
|
|
}))
|
|
.into()
|
|
}
|
|
|
|
pub(super) fn bar<'a>(
|
|
shell: &'a FereseShell,
|
|
palette: ShellTheme,
|
|
metrics: BarMetrics,
|
|
representation: crate::panel::Representation,
|
|
selected: bool,
|
|
width: Option<f32>,
|
|
) -> Element<'a, cosmic::Action<Message>> {
|
|
let Some(player) = &shell.media.snapshot.selected else {
|
|
return shell.render.text("").into();
|
|
};
|
|
let foreground = color(if selected { palette.accent } else { palette.text_primary });
|
|
let vertical_padding = ((metrics.group_item_height - f32::from(metrics.icon_size)) * 0.5).clamp(0.0, 2.0);
|
|
let height = metrics.group_item_height - 2.0 * vertical_padding;
|
|
let side = height.min(24.0);
|
|
let cover: Element<'_, cosmic::Action<Message>> =
|
|
match shell.media.artwork.as_ref().and_then(|art| art.handle.as_ref()) {
|
|
Some(handle) => image(handle.clone())
|
|
.width(side)
|
|
.height(side)
|
|
.content_fit(cosmic::iced::ContentFit::Cover)
|
|
.border_radius(shell.render.radius(6.0))
|
|
.shape(BorderShape::Continuous)
|
|
.into(),
|
|
None => container(accented_icon(
|
|
icons::MEDIA,
|
|
side as u16,
|
|
foreground,
|
|
color(palette.accent),
|
|
))
|
|
.center_x(side)
|
|
.center_y(side)
|
|
.width(side)
|
|
.into(),
|
|
};
|
|
let label = shell
|
|
.render
|
|
.text(player.label())
|
|
.size(metrics.text_size)
|
|
.wrapping(cosmic::iced::widget::text::Wrapping::None)
|
|
.ellipsize(cosmic::iced::widget::text::Ellipsize::End(
|
|
cosmic::iced::advanced::text::EllipsizeHeightLimit::Lines(1),
|
|
))
|
|
.width(Length::Shrink)
|
|
.height(height)
|
|
.align_y(cosmic::iced::alignment::Vertical::Center)
|
|
.class(theme::Text::Color(foreground));
|
|
let compact = representation == crate::panel::Representation::Compact;
|
|
let preferred_label_width: f32 = if compact { 112. } else { 320. };
|
|
// Outer padding, the transport button, details padding, and cover spacing
|
|
// share the allocation with the label.
|
|
let chrome = 4. + 2. + f32::from(metrics.icon_size) + 12. + 8. + if compact { 0. } else { side + 8. };
|
|
let label_width = width.map_or(preferred_label_width, |width| {
|
|
(width - chrome).max(0.).min(preferred_label_width)
|
|
});
|
|
let label = container(label).max_width(label_width);
|
|
let details = button::custom(bar_content(
|
|
if representation == crate::panel::Representation::Compact {
|
|
row![label].align_y(Alignment::Center)
|
|
} else {
|
|
row![cover, label].spacing(8).align_y(Alignment::Center)
|
|
},
|
|
height,
|
|
))
|
|
.name(format!("Now playing: {}", player.label()))
|
|
.padding([0, 4])
|
|
.height(height)
|
|
.on_press_with_rectangle(move |offset, bounds| {
|
|
cosmic::Action::App(Message::OpenMenu(
|
|
Menu::Media,
|
|
Rectangle {
|
|
x: (bounds.x - offset.x).round() as i32,
|
|
y: (bounds.y - offset.y).round() as i32,
|
|
width: bounds.width.round() as i32,
|
|
height: bounds.height.round() as i32,
|
|
},
|
|
))
|
|
});
|
|
let playing = player.status == Playback::Playing;
|
|
let mut toggle = button::custom(bar_content(
|
|
accented_icon(
|
|
if playing { icons::MEDIA_PAUSE } else { icons::MEDIA_PLAY },
|
|
metrics.icon_size,
|
|
foreground,
|
|
color(palette.accent),
|
|
),
|
|
height,
|
|
))
|
|
.name(if playing { "Pause" } else { "Play" })
|
|
.height(height)
|
|
.padding([0, 6]);
|
|
if player.can_toggle() {
|
|
toggle = toggle.on_press(cosmic::Action::App(Message::MediaAction(
|
|
shell.media.action("play-pause"),
|
|
)));
|
|
}
|
|
|
|
let bar_style = ferese_theme::controls::surface_appearance(
|
|
color_with_opacity(palette.border, 0.30),
|
|
palette.material_radius.min(16.),
|
|
);
|
|
|
|
let controls = container(
|
|
row![
|
|
motion::button(shell.render, details, foreground, false, 1.0),
|
|
motion::button(shell.render, toggle, foreground, false, 1.0)
|
|
]
|
|
.spacing(2)
|
|
.align_y(Alignment::Center),
|
|
)
|
|
.padding([vertical_padding, 2.0])
|
|
.height(metrics.group_item_height)
|
|
.align_y(cosmic::iced::alignment::Vertical::Center)
|
|
.class(theme::Container::custom(move |_| bar_style));
|
|
|
|
let mut area = mouse_area(controls);
|
|
if player.can_raise {
|
|
area = area.on_middle_release(cosmic::Action::App(Message::MediaAction(shell.media.action("raise"))));
|
|
}
|
|
if player.can_control && player.volume.is_some() {
|
|
let action = shell.media.action("volume");
|
|
area = area.on_scroll(move |delta| {
|
|
let mut action = action.clone();
|
|
action["delta"] = json!(volume_delta(delta));
|
|
cosmic::Action::App(Message::MediaAction(action))
|
|
});
|
|
}
|
|
|
|
area.into()
|
|
}
|
|
|
|
fn volume_delta(delta: mouse::ScrollDelta) -> f64 {
|
|
let delta = match delta {
|
|
mouse::ScrollDelta::Lines { y, .. } => f64::from(y) * 0.05,
|
|
mouse::ScrollDelta::Pixels { y, .. } => f64::from(y) * 0.001,
|
|
};
|
|
if delta.is_finite() { delta.clamp(-1.0, 1.0) } else { 0.0 }
|
|
}
|
|
|
|
fn transport<'a>(
|
|
shell: &'a FereseShell,
|
|
source: &'static [u8],
|
|
label: &'static str,
|
|
action: &str,
|
|
enabled: bool,
|
|
style: MenuStyle,
|
|
) -> Element<'a, cosmic::Action<Message>> {
|
|
transport_sized(shell, source, label, action, enabled, style, 44.)
|
|
}
|
|
|
|
fn transport_sized<'a>(
|
|
shell: &'a FereseShell,
|
|
source: &'static [u8],
|
|
label: &'static str,
|
|
action: &str,
|
|
enabled: bool,
|
|
style: MenuStyle,
|
|
size: f32,
|
|
) -> Element<'a, cosmic::Action<Message>> {
|
|
let fill = (action == "play-pause").then_some(if enabled { style.primary } else { style.muted });
|
|
let foreground = match fill {
|
|
Some(fill) => cosmic::iced::Color {
|
|
a: fill.a,
|
|
..ferese_theme::foreground(fill, color(style.theme.surface_popover))
|
|
},
|
|
None => {
|
|
if enabled {
|
|
style.primary
|
|
} else {
|
|
style.muted
|
|
}
|
|
}
|
|
};
|
|
let mut control = button::custom(
|
|
container(
|
|
accented_icon(
|
|
source,
|
|
(size - 20.) as u16,
|
|
cosmic::iced::Color { a: 1., ..foreground },
|
|
color(style.theme.accent),
|
|
)
|
|
.opacity(foreground.a),
|
|
)
|
|
.center_x(size - 20.)
|
|
.center_y(size - 20.),
|
|
)
|
|
.padding(10)
|
|
.width(size)
|
|
.height(size)
|
|
.name(label);
|
|
if enabled {
|
|
control = control.on_press(cosmic::Action::App(Message::MediaAction(shell.media.action(action))));
|
|
}
|
|
|
|
motion::circular_button(shell.render, control, foreground, fill, style.opacity, size)
|
|
}
|
|
|
|
pub(super) fn hub<'a>(shell: &'a FereseShell, rows: MenuRows<'a>, style: MenuStyle) -> MenuRows<'a> {
|
|
let Some(player) = &shell.media.snapshot.selected else {
|
|
return rows.push(
|
|
shell
|
|
.render
|
|
.text("No active player")
|
|
.size(14)
|
|
.class(theme::Text::Color(style.muted)),
|
|
);
|
|
};
|
|
let cover: Element<'_, cosmic::Action<Message>> =
|
|
match shell.media.artwork.as_ref().and_then(|art| art.handle.as_ref()) {
|
|
Some(handle) => image(handle.clone())
|
|
.width(64)
|
|
.height(64)
|
|
.opacity(style.opacity)
|
|
.content_fit(cosmic::iced::ContentFit::Cover)
|
|
.border_radius(10.)
|
|
.into(),
|
|
None => container(
|
|
accented_icon(
|
|
icons::MEDIA,
|
|
26,
|
|
color(style.theme.text_primary),
|
|
color(style.theme.accent),
|
|
)
|
|
.opacity(style.opacity),
|
|
)
|
|
.center_x(64)
|
|
.center_y(64)
|
|
.class(theme::Container::custom(move |_| {
|
|
ferese_theme::controls::surface_appearance(style.primary.scale_alpha(0.06), 10.)
|
|
}))
|
|
.into(),
|
|
};
|
|
let mut info = column![
|
|
shell
|
|
.render
|
|
.text(player.label())
|
|
.size(14)
|
|
.class(theme::Text::Color(style.primary))
|
|
.width(Length::Fill)
|
|
.ellipsize(cosmic::iced::widget::text::Ellipsize::End(
|
|
cosmic::iced::advanced::text::EllipsizeHeightLimit::Lines(1)
|
|
)),
|
|
shell
|
|
.render
|
|
.text(&player.artist)
|
|
.size(12)
|
|
.class(theme::Text::Color(style.muted))
|
|
.width(Length::Fill)
|
|
.ellipsize(cosmic::iced::widget::text::Ellipsize::End(
|
|
cosmic::iced::advanced::text::EllipsizeHeightLimit::Lines(1)
|
|
))
|
|
]
|
|
.spacing(4)
|
|
.width(Length::Fill);
|
|
if let (Some(length), Some(position)) = (
|
|
player.length_us.filter(|length| *length > 0),
|
|
player.position_at(shell.media.now_us),
|
|
) {
|
|
let value =
|
|
(shell.media.seeking.unwrap_or(position).min(length) as f64 / length as f64 * 10_000.).round() as u32;
|
|
if player.can_control && player.can_seek && player.track_id.is_some() {
|
|
info = info.push(
|
|
slider(0..=10_000_u32, value, move |value| {
|
|
cosmic::Action::App(Message::MediaSeek(seek_position(length, value)))
|
|
})
|
|
.on_release(cosmic::Action::App(Message::MediaSeekCommit))
|
|
.height(16)
|
|
.handle_width(SEEK_THUMB_DIAMETER)
|
|
.class(seek_slider_style(style))
|
|
.width(Length::Fill),
|
|
);
|
|
}
|
|
}
|
|
let rows = rows.push(row![cover, info].spacing(12).align_y(Alignment::Center));
|
|
let playing = player.status == Playback::Playing;
|
|
rows.push(
|
|
container(
|
|
row![
|
|
transport_sized(
|
|
shell,
|
|
icons::MEDIA_PREVIOUS,
|
|
"Previous track",
|
|
"previous",
|
|
player.can_control && player.can_previous,
|
|
style,
|
|
36.
|
|
),
|
|
transport_sized(
|
|
shell,
|
|
if playing { icons::MEDIA_PAUSE } else { icons::MEDIA_PLAY },
|
|
if playing { "Pause" } else { "Play" },
|
|
"play-pause",
|
|
player.can_toggle(),
|
|
style,
|
|
36.
|
|
),
|
|
transport_sized(
|
|
shell,
|
|
icons::MEDIA_NEXT,
|
|
"Next track",
|
|
"next",
|
|
player.can_control && player.can_next,
|
|
style,
|
|
36.
|
|
),
|
|
]
|
|
.spacing(8),
|
|
)
|
|
.center_x(Length::Fill),
|
|
)
|
|
}
|
|
|
|
pub(super) fn view<'a>(shell: &'a FereseShell, mut rows: MenuRows<'a>, style: MenuStyle) -> MenuRows<'a> {
|
|
if let Some(player) = &shell.media.snapshot.selected {
|
|
let cover: Element<'_, cosmic::Action<Message>> =
|
|
match shell.media.artwork.as_ref().and_then(|art| art.handle.as_ref()) {
|
|
Some(handle) => image(handle.clone())
|
|
.width(64)
|
|
.height(64)
|
|
.content_fit(cosmic::iced::ContentFit::Contain)
|
|
.border_radius(shell.render.radius(12.0))
|
|
.shape(BorderShape::Continuous)
|
|
.into(),
|
|
None => container(accented_icon(icons::MEDIA, 30, style.muted, color(style.theme.accent)))
|
|
.center_x(64)
|
|
.center_y(64)
|
|
.into(),
|
|
};
|
|
let text_size = if player.label().len() >= 24 { 13 } else { 15 };
|
|
let mut info = column![
|
|
shell
|
|
.render
|
|
.text(player.label())
|
|
.size(text_size)
|
|
.width(Length::Fill)
|
|
.ellipsize(cosmic::iced::widget::text::Ellipsize::End(
|
|
cosmic::iced::advanced::text::EllipsizeHeightLimit::Lines(2)
|
|
))
|
|
]
|
|
.spacing(4)
|
|
.width(Length::Fill);
|
|
if !player.artist.is_empty() {
|
|
info = info.push(
|
|
shell
|
|
.render
|
|
.text(&player.artist)
|
|
.size(12)
|
|
.width(Length::Fill)
|
|
.ellipsize(cosmic::iced::widget::text::Ellipsize::End(
|
|
cosmic::iced::advanced::text::EllipsizeHeightLimit::Lines(1),
|
|
))
|
|
.class(theme::Text::Color(style.muted)),
|
|
);
|
|
}
|
|
if !player.album.is_empty() {
|
|
info = info.push(
|
|
shell
|
|
.render
|
|
.text(&player.album)
|
|
.size(11)
|
|
.width(Length::Fill)
|
|
.ellipsize(cosmic::iced::widget::text::Ellipsize::End(
|
|
cosmic::iced::advanced::text::EllipsizeHeightLimit::Lines(1),
|
|
))
|
|
.class(theme::Text::Color(style.muted)),
|
|
);
|
|
}
|
|
|
|
rows = rows.push(row![cover, info].spacing(12).align_y(Alignment::Center));
|
|
let mut playback = column::with_capacity(2).spacing(6).width(Length::Fill);
|
|
if let (Some(length), Some(position)) = (
|
|
player.length_us.filter(|length| *length > 0),
|
|
player.position_at(shell.media.now_us),
|
|
) {
|
|
let position = shell.media.seeking.unwrap_or(position).min(length);
|
|
let mut progress = column::with_capacity(2).spacing(2);
|
|
if player.can_control && player.can_seek && player.track_id.is_some() {
|
|
let value = (position as f64 / length as f64 * 10_000.0).round() as u32;
|
|
progress = progress.push(seek_pill(
|
|
container(
|
|
slider(0..=10_000_u32, value, move |value| {
|
|
cosmic::Action::App(Message::MediaSeek(seek_position(length, value)))
|
|
})
|
|
.on_release(cosmic::Action::App(Message::MediaSeekCommit))
|
|
.handle_width(SEEK_THUMB_DIAMETER)
|
|
.height(20)
|
|
.class(seek_slider_style(style))
|
|
.width(Length::Fill),
|
|
)
|
|
// Extend the native slider's layout by its reserved thumb
|
|
// radius so its rail and pointer mapping reach both edges.
|
|
.padding(cosmic::iced::Padding {
|
|
left: -SEEK_THUMB_DIAMETER / 2.0,
|
|
right: -SEEK_THUMB_DIAMETER / 2.0,
|
|
..Default::default()
|
|
}),
|
|
style,
|
|
));
|
|
} else {
|
|
progress = progress.push(seek_pill(
|
|
container(
|
|
cosmic::iced::widget::ProgressBar::new(0.0..=1.0, (position as f64 / length as f64) as f32)
|
|
.girth(4)
|
|
.length(Length::Fill)
|
|
.class(cosmic::theme::iced::ProgressBar::custom(move |_| {
|
|
cosmic::iced::widget::progress_bar::Style {
|
|
background: style.muted.scale_alpha(0.35).into(),
|
|
bar: color_with_opacity(style.theme.accent, style.opacity).into(),
|
|
border: cosmic::iced::Border {
|
|
shape: BorderShape::Circular,
|
|
radius: 2.0.into(),
|
|
..Default::default()
|
|
},
|
|
}
|
|
})),
|
|
)
|
|
.center_y(20),
|
|
style,
|
|
));
|
|
}
|
|
|
|
progress = progress.push(
|
|
row![
|
|
shell
|
|
.render
|
|
.text(time_label(position))
|
|
.font(cosmic::iced::Font::MONOSPACE)
|
|
.size(11)
|
|
.class(theme::Text::Color(style.muted))
|
|
.width(Length::Fill),
|
|
shell
|
|
.render
|
|
.text(time_label(length))
|
|
.font(cosmic::iced::Font::MONOSPACE)
|
|
.size(11)
|
|
.class(theme::Text::Color(style.muted))
|
|
]
|
|
.spacing(12),
|
|
);
|
|
playback = playback.push(progress);
|
|
}
|
|
|
|
let playing = player.status == Playback::Playing;
|
|
playback = playback.push(
|
|
container(
|
|
row![
|
|
transport(
|
|
shell,
|
|
icons::MEDIA_PREVIOUS,
|
|
"Previous track",
|
|
"previous",
|
|
player.can_control && player.can_previous,
|
|
style
|
|
),
|
|
transport(
|
|
shell,
|
|
if playing { icons::MEDIA_PAUSE } else { icons::MEDIA_PLAY },
|
|
if playing { "Pause" } else { "Play" },
|
|
"play-pause",
|
|
player.can_toggle(),
|
|
style
|
|
),
|
|
transport(
|
|
shell,
|
|
icons::MEDIA_NEXT,
|
|
"Next track",
|
|
"next",
|
|
player.can_control && player.can_next,
|
|
style
|
|
),
|
|
]
|
|
.spacing(12),
|
|
)
|
|
.center_x(Length::Fill),
|
|
);
|
|
rows = rows.push(playback);
|
|
|
|
let label = if shell.media.snapshot.pinned.is_some() {
|
|
format!("{} · pinned", player.identity)
|
|
} else {
|
|
player.identity.clone()
|
|
};
|
|
rows = rows.push(motion::button(
|
|
shell.render,
|
|
button::custom(shell.render.text(label).size(12))
|
|
.padding([6, 8])
|
|
.on_press(cosmic::Action::App(Message::MediaChoose)),
|
|
style.primary,
|
|
false,
|
|
style.opacity,
|
|
));
|
|
} else {
|
|
rows = rows.push(
|
|
shell
|
|
.render
|
|
.text("No active player")
|
|
.size(13)
|
|
.class(theme::Text::Color(style.muted)),
|
|
);
|
|
}
|
|
|
|
if shell.media.choosing || shell.media.snapshot.selected.is_none() {
|
|
let mut choices = column::with_capacity(shell.media.snapshot.players.len() + 1).spacing(4);
|
|
choices = choices.push(super::controls::menu_button(
|
|
shell.render,
|
|
"Choose automatically",
|
|
Message::MediaAction(json!({"action":"auto"})),
|
|
style.primary,
|
|
style.opacity,
|
|
));
|
|
for player in &shell.media.snapshot.players {
|
|
let args = json!({"player":player.name,"owner":player.owner});
|
|
let mut pin = args.clone();
|
|
pin["action"] = json!("pin");
|
|
let mut ignore = args;
|
|
ignore["action"] = json!("ignore");
|
|
ignore["ignored"] = json!(!player.ignored);
|
|
let label = if player.identity.is_empty() {
|
|
&player.name
|
|
} else {
|
|
&player.identity
|
|
};
|
|
choices = choices.push(
|
|
row![
|
|
super::controls::menu_button(
|
|
shell.render,
|
|
label,
|
|
Message::MediaAction(pin),
|
|
if player.ignored { style.muted } else { style.primary },
|
|
style.opacity
|
|
),
|
|
super::controls::menu_button(
|
|
shell.render,
|
|
if player.ignored { "Show" } else { "Ignore" },
|
|
Message::MediaAction(ignore),
|
|
style.muted,
|
|
style.opacity
|
|
),
|
|
]
|
|
.spacing(6),
|
|
);
|
|
}
|
|
|
|
rows = rows.push(scrollable(choices).height(Length::Fixed(140.0)));
|
|
}
|
|
|
|
if let Some(error) = &shell.media.error {
|
|
rows = rows.push(shell.render.text(error).size(12).class(theme::Text::Color(style.muted)));
|
|
}
|
|
|
|
rows
|
|
}
|
|
|
|
fn seek_position(length: u64, value: u32) -> u64 {
|
|
(u128::from(length) * u128::from(value.min(10_000)) / 10_000) as u64
|
|
}
|
|
|
|
fn time_label(us: u64) -> String {
|
|
let seconds = us / 1_000_000;
|
|
format!("{}:{:02}", seconds / 60, seconds % 60)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn seeking_handles_short_tracks_and_large_durations() {
|
|
assert_eq!(seek_position(500_000, 5000), 250_000);
|
|
assert_eq!(seek_position(500_000, 10_000), 500_000);
|
|
assert_eq!(seek_position(u64::MAX, 10_000), u64::MAX);
|
|
assert_eq!(seek_position(1, 100_000), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn wheel_changes_are_bounded_and_pixel_scrolling_is_not_a_full_step() {
|
|
assert_eq!(volume_delta(mouse::ScrollDelta::Lines { x: 0.0, y: 1.0 }), 0.05);
|
|
assert_eq!(volume_delta(mouse::ScrollDelta::Pixels { x: 0.0, y: 1.0 }), 0.001);
|
|
assert_eq!(volume_delta(mouse::ScrollDelta::Lines { x: 0.0, y: f32::NAN }), 0.0);
|
|
assert_eq!(volume_delta(mouse::ScrollDelta::Lines { x: 0.0, y: 1000.0 }), 1.0);
|
|
}
|
|
}
|