use std::sync::atomic::{AtomicU64, Ordering};
use crate::app_state::with_app_state;
use crate::outputs::OutputId;
use crate::platform::{Anchor, KeyboardInteractivity, Layer};
use crate::reactive::Trigger;
use crate::reactive::global::GlobalSignal;
use crate::widgets::{Color, Rect, Widget};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SurfaceId(u64);
impl SurfaceId {
pub fn next() -> Self {
static COUNTER: AtomicU64 = AtomicU64::new(0);
SurfaceId(COUNTER.fetch_add(1, Ordering::Relaxed))
}
pub fn raw(&self) -> u64 {
self.0
}
}
#[derive(Clone)]
pub struct SurfaceConfig {
pub width: SurfaceExtent,
pub height: SurfaceExtent,
pub anchor: Anchor,
pub layer: Layer,
pub keyboard_interactivity: KeyboardInteractivity,
pub namespace: String,
pub background_color: Color,
pub exclusive_zone: ExclusiveZone,
pub margin: Margin,
pub output: Option<OutputId>,
pub input_region: Option<Vec<Rect>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Margin {
pub top: i32,
pub right: i32,
pub bottom: i32,
pub left: i32,
}
impl Margin {
pub fn all(value: i32) -> Self {
Self {
top: value,
right: value,
bottom: value,
left: value,
}
}
pub(crate) fn is_zero(self) -> bool {
self == Self::default()
}
}
impl From<i32> for Margin {
fn from(v: i32) -> Self {
Margin::all(v)
}
}
impl From<u32> for Margin {
fn from(v: u32) -> Self {
Margin::all(v as i32)
}
}
impl From<[i32; 2]> for Margin {
fn from(v: [i32; 2]) -> Self {
Margin {
top: v[0],
right: v[1],
bottom: v[0],
left: v[1],
}
}
}
impl From<[i32; 4]> for Margin {
fn from(v: [i32; 4]) -> Self {
Margin {
top: v[0],
right: v[1],
bottom: v[2],
left: v[3],
}
}
}
impl From<[u32; 2]> for Margin {
fn from(v: [u32; 2]) -> Self {
Margin::from([v[0] as i32, v[1] as i32])
}
}
impl From<[u32; 4]> for Margin {
fn from(v: [u32; 4]) -> Self {
Margin::from([v[0] as i32, v[1] as i32, v[2] as i32, v[3] as i32])
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SurfaceExtent {
Fixed(u32),
Content,
}
pub(crate) fn compositor_owned_axes(anchor: Anchor) -> (bool, bool) {
(
anchor.contains(Anchor::LEFT) && anchor.contains(Anchor::RIGHT),
anchor.contains(Anchor::TOP) && anchor.contains(Anchor::BOTTOM),
)
}
pub(crate) fn warn_content_on_stretched_axis(id: SurfaceId, config: &SurfaceConfig) {
let (stretch_w, stretch_h) = compositor_owned_axes(config.anchor);
if (stretch_w && config.width.is_content()) || (stretch_h && config.height.is_content()) {
log::warn!(
"Surface {id:?}: content() sizing on an axis anchored to both screen \
edges is compositor-owned and will be ignored"
);
}
}
pub(crate) fn warn_size_request_on_stretched_axis(id: SurfaceId, config: &SurfaceConfig) {
let (stretch_w, stretch_h) = compositor_owned_axes(config.anchor);
for (stretched, extent, axis) in [
(stretch_w, config.width, "width"),
(stretch_h, config.height, "height"),
] {
if !stretched {
continue;
}
match extent {
SurfaceExtent::Content => log::warn!(
"Surface {id:?}: content() {axis} on an axis anchored to both \
screen edges is compositor-owned and will be ignored"
),
SurfaceExtent::Fixed(v) => log::warn!(
"Surface {id:?}: {axis} of {v} on an axis anchored to both screen \
edges is compositor-owned and will be ignored"
),
}
}
}
pub(crate) fn requested_extent(extent: SurfaceExtent, live: Option<u32>) -> u32 {
match extent {
SurfaceExtent::Fixed(v) => v,
SurfaceExtent::Content => live.unwrap_or_else(|| extent.initial()),
}
}
pub(crate) fn resize_request(config: &SurfaceConfig, live: Option<(u32, u32)>) -> (u32, u32, bool) {
let (width, height) = honour_owned_axes(
config.anchor,
requested_extent(config.width, live.map(|(w, _)| w)),
requested_extent(config.height, live.map(|(_, h)| h)),
);
(width, height, needs_content_measure(config))
}
pub(crate) fn measure_pins(
config: &SurfaceConfig,
confirmed: (u32, u32),
) -> (Option<u32>, Option<u32>) {
let (owned_w, owned_h) = compositor_owned_axes(config.anchor);
let pin = |extent: SurfaceExtent, owned: bool, confirmed: u32| match extent {
SurfaceExtent::Content => None,
SurfaceExtent::Fixed(_) if owned => Some(confirmed),
SurfaceExtent::Fixed(v) => Some(v),
};
(
pin(config.width, owned_w, confirmed.0),
pin(config.height, owned_h, confirmed.1),
)
}
pub(crate) struct InitialDeclaration {
pub asked: (u32, u32),
pub believed: (u32, u32),
pub exclusive_zone: i32,
pub input_region: Option<Vec<Rect>>,
}
pub(crate) fn initial_declaration(config: &SurfaceConfig) -> InitialDeclaration {
let (ask_w, ask_h, _) = resize_request(config, None);
let believed = (
requested_extent(config.width, None),
requested_extent(config.height, None),
);
InitialDeclaration {
asked: (ask_w, ask_h),
believed,
exclusive_zone: config.exclusive_zone.resolve(
config.anchor,
config.margin,
believed.0,
believed.1,
),
input_region: config.input_region.clone(),
}
}
pub(crate) fn needs_content_measure(config: &SurfaceConfig) -> bool {
let (owns_w, owns_h) = compositor_owned_axes(config.anchor);
(config.width.is_content() && !owns_w) || (config.height.is_content() && !owns_h)
}
pub(crate) fn honour_owned_axes(anchor: Anchor, width: u32, height: u32) -> (u32, u32) {
let (stretch_w, stretch_h) = compositor_owned_axes(anchor);
(
if stretch_w { 0 } else { width },
if stretch_h { 0 } else { height },
)
}
pub(crate) fn buffer_size((width, height): (u32, u32), scale: f32) -> (u32, u32) {
let scale = f64::from(scale);
(
(f64::from(width) * scale).round() as u32,
(f64::from(height) * scale).round() as u32,
)
}
pub(crate) fn adopted_scale(held: f32, named: f32) -> (f32, bool) {
let named = named.max(1.0);
(named, held != named)
}
#[derive(Clone, Copy, Debug, Default)]
pub(crate) struct FramePacing {
callback_pending: bool,
presented: bool,
}
impl FramePacing {
pub(crate) fn committed(&mut self) {
self.callback_pending = true;
self.presented = true;
}
pub(crate) fn callback_done(&mut self) {
self.callback_pending = false;
}
pub(crate) fn callback_pending(self) -> bool {
self.callback_pending
}
pub(crate) fn forced(self) -> bool {
!self.presented
}
}
impl SurfaceExtent {
pub(crate) fn initial(self) -> u32 {
match self {
SurfaceExtent::Fixed(v) => v,
SurfaceExtent::Content => 1,
}
}
pub(crate) fn is_content(self) -> bool {
matches!(self, SurfaceExtent::Content)
}
}
impl From<u32> for SurfaceExtent {
fn from(v: u32) -> Self {
SurfaceExtent::Fixed(v)
}
}
impl From<i32> for SurfaceExtent {
fn from(v: i32) -> Self {
SurfaceExtent::Fixed(v.max(0) as u32)
}
}
pub fn content() -> SurfaceExtent {
SurfaceExtent::Content
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExclusiveZone {
Auto,
Fixed(u32),
None,
Ignore,
}
impl ExclusiveZone {
pub(crate) fn resolve(self, anchor: Anchor, margin: Margin, width: u32, height: u32) -> i32 {
match self {
ExclusiveZone::Auto => match Self::follow_axis(anchor) {
Some(FollowAxis::Height) => {
let inner_margin = if anchor.contains(Anchor::TOP) {
margin.bottom
} else {
margin.top
};
height as i32 + inner_margin
}
Some(FollowAxis::Width) => {
let inner_margin = if anchor.contains(Anchor::LEFT) {
margin.right
} else {
margin.left
};
width as i32 + inner_margin
}
None => {
log::warn!(
"ExclusiveZone::Auto on a corner/full anchor has no \
meaningful axis (layer-shell spec); reserving nothing"
);
0
}
},
ExclusiveZone::Fixed(z) => z as i32,
ExclusiveZone::None => 0,
ExclusiveZone::Ignore => -1,
}
}
pub(crate) fn follow_axis(anchor: Anchor) -> Option<FollowAxis> {
let vertical_edge = anchor.contains(Anchor::TOP) != anchor.contains(Anchor::BOTTOM);
let horizontal_edge = anchor.contains(Anchor::LEFT) != anchor.contains(Anchor::RIGHT);
match (vertical_edge, horizontal_edge) {
(true, false) => Some(FollowAxis::Height),
(false, true) => Some(FollowAxis::Width),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum FollowAxis {
Width,
Height,
}
impl From<u32> for ExclusiveZone {
fn from(z: u32) -> Self {
ExclusiveZone::Fixed(z)
}
}
impl Default for SurfaceConfig {
fn default() -> Self {
Self {
width: SurfaceExtent::Fixed(400),
height: SurfaceExtent::Fixed(300),
anchor: Anchor::empty(),
layer: Layer::Top,
keyboard_interactivity: KeyboardInteractivity::OnDemand,
namespace: "guido-surface".to_string(),
background_color: Color::rgb(0.1, 0.1, 0.15),
exclusive_zone: ExclusiveZone::None,
margin: Margin::default(),
output: None,
input_region: None,
}
}
}
impl SurfaceConfig {
pub fn new() -> Self {
Self::default()
}
pub fn width(mut self, width: impl Into<SurfaceExtent>) -> Self {
self.width = width.into();
self
}
pub fn height(mut self, height: impl Into<SurfaceExtent>) -> Self {
self.height = height.into();
self
}
pub fn anchor(mut self, anchor: Anchor) -> Self {
self.anchor = anchor;
self
}
pub fn layer(mut self, layer: Layer) -> Self {
self.layer = layer;
self
}
pub fn namespace(mut self, namespace: impl Into<String>) -> Self {
self.namespace = namespace.into();
self
}
pub fn background_color(mut self, color: Color) -> Self {
self.background_color = color;
self
}
pub fn exclusive_zone(mut self, zone: impl Into<ExclusiveZone>) -> Self {
self.exclusive_zone = zone.into();
self
}
pub fn keyboard_interactivity(mut self, mode: KeyboardInteractivity) -> Self {
self.keyboard_interactivity = mode;
self
}
pub fn margin(mut self, margin: impl Into<Margin>) -> Self {
self.margin = margin.into();
self
}
pub fn output(mut self, output: OutputId) -> Self {
self.output = Some(output);
self
}
pub fn input_region(mut self, rects: impl Into<Vec<Rect>>) -> Self {
self.input_region = Some(rects.into());
self
}
pub fn click_through(mut self) -> Self {
self.input_region = Some(Vec::new());
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PopupAnchor {
#[default]
None,
Top,
Bottom,
Left,
Right,
TopLeft,
BottomLeft,
TopRight,
BottomRight,
}
pub type PopupGravity = PopupAnchor;
#[derive(Clone)]
pub struct PopupConfig {
pub width: u32,
pub height: Option<u32>,
pub anchor_rect: Rect,
pub anchor: PopupAnchor,
pub gravity: PopupGravity,
pub offset: (i32, i32),
pub grab: bool,
pub background_color: Color,
}
impl PopupConfig {
pub fn new(width: u32) -> Self {
Self {
width,
height: None,
anchor_rect: Rect::new(0.0, 0.0, 1.0, 1.0),
anchor: PopupAnchor::Bottom,
gravity: PopupGravity::Bottom,
offset: (0, 0),
grab: false,
background_color: Color::TRANSPARENT,
}
}
pub fn height(mut self, height: u32) -> Self {
self.height = Some(height);
self
}
pub fn anchor_rect(mut self, rect: Rect) -> Self {
self.anchor_rect = rect;
self
}
pub fn anchor(mut self, anchor: PopupAnchor) -> Self {
self.anchor = anchor;
self
}
pub fn gravity(mut self, gravity: PopupGravity) -> Self {
self.gravity = gravity;
self
}
pub fn offset(mut self, x: i32, y: i32) -> Self {
self.offset = (x, y);
self
}
pub fn grab(mut self) -> Self {
self.grab = true;
self
}
pub fn background_color(mut self, color: Color) -> Self {
self.background_color = color;
self
}
}
#[derive(Clone)]
pub struct SurfaceHandle {
id: SurfaceId,
}
impl SurfaceHandle {
pub fn close(&self) {
push_surface_command(SurfaceCommand::Close(self.id));
}
pub fn id(&self) -> SurfaceId {
self.id
}
pub fn set_layer(&self, layer: Layer) {
push_surface_command(SurfaceCommand::SetLayer { id: self.id, layer });
}
pub fn set_keyboard_interactivity(&self, mode: KeyboardInteractivity) {
push_surface_command(SurfaceCommand::SetKeyboardInteractivity { id: self.id, mode });
}
pub fn set_anchor(&self, anchor: Anchor) {
push_surface_command(SurfaceCommand::SetAnchor {
id: self.id,
anchor,
});
}
pub fn set_size(&self, width: impl Into<SurfaceExtent>, height: impl Into<SurfaceExtent>) {
push_surface_command(SurfaceCommand::SetSize {
id: self.id,
width: width.into(),
height: height.into(),
});
}
pub fn set_exclusive_zone(&self, zone: impl Into<ExclusiveZone>) {
push_surface_command(SurfaceCommand::SetExclusiveZone {
id: self.id,
zone: zone.into(),
});
}
pub fn set_margin(&self, margin: impl Into<Margin>) {
push_surface_command(SurfaceCommand::SetMargin {
id: self.id,
margin: margin.into(),
});
}
pub fn set_input_region(&self, rects: Option<Vec<Rect>>) {
push_surface_command(SurfaceCommand::SetInputRegion { id: self.id, rects });
}
}
#[allow(clippy::type_complexity)]
pub(crate) enum SurfaceCommand {
Create {
id: SurfaceId,
config: SurfaceConfig,
widget_fn: Box<dyn FnOnce() -> Box<dyn Widget>>,
},
Close(SurfaceId),
SetLayer { id: SurfaceId, layer: Layer },
SetKeyboardInteractivity {
id: SurfaceId,
mode: KeyboardInteractivity,
},
SetAnchor { id: SurfaceId, anchor: Anchor },
SetSize {
id: SurfaceId,
width: SurfaceExtent,
height: SurfaceExtent,
},
SetExclusiveZone { id: SurfaceId, zone: ExclusiveZone },
SetMargin { id: SurfaceId, margin: Margin },
SetInputRegion {
id: SurfaceId,
rects: Option<Vec<Rect>>,
},
CreatePopup {
id: SurfaceId,
parent: SurfaceId,
config: PopupConfig,
widget_fn: Box<dyn FnOnce() -> Box<dyn Widget>>,
},
}
pub(crate) fn push_surface_command(cmd: SurfaceCommand) {
with_app_state(|app| app.surface_commands.push(cmd));
}
pub(crate) fn drain_surface_commands() -> Vec<SurfaceCommand> {
with_app_state(|app| app.surface_commands.drain())
}
pub(crate) fn closes_requested() -> Vec<SurfaceId> {
with_app_state(|app| {
app.surface_commands.peek(|queued| {
queued
.iter()
.filter_map(|cmd| match cmd {
SurfaceCommand::Close(id) => Some(*id),
_ => None,
})
.collect()
})
})
}
pub fn spawn_surface<W, F>(config: SurfaceConfig, widget_fn: F) -> SurfaceHandle
where
W: Widget + 'static,
F: FnOnce() -> W + 'static,
{
let id = SurfaceId::next();
push_surface_command(SurfaceCommand::Create {
id,
config,
widget_fn: Box::new(move || Box::new(widget_fn())),
});
SurfaceHandle { id }
}
#[derive(Clone, Copy)]
pub struct PopupHandle {
id: SurfaceId,
}
impl PopupHandle {
pub fn id(&self) -> SurfaceId {
self.id
}
pub fn close(&self) {
push_surface_command(SurfaceCommand::Close(self.id));
}
pub fn dismissed(&self) -> bool {
let live = with_app_state(|app| app.live_popups.borrow().contains(&self.id));
if live {
popup_dismissal().track();
}
!live
}
}
static POPUP_DISMISSAL: GlobalSignal<()> = GlobalSignal::new(|| ());
fn popup_dismissal() -> Trigger {
Trigger::from_signal(POPUP_DISMISSAL.get())
}
pub(crate) fn mark_popup_dismissed(id: SurfaceId) {
let was_open = with_app_state(|app| app.live_popups.borrow_mut().remove(&id));
if was_open {
popup_dismissal().notify();
}
}
pub fn spawn_popup<W, F>(parent: SurfaceId, config: PopupConfig, widget_fn: F) -> PopupHandle
where
W: Widget + 'static,
F: FnOnce() -> W + 'static,
{
let id = SurfaceId::next();
with_app_state(|app| app.live_popups.borrow_mut().insert(id));
push_surface_command(SurfaceCommand::CreatePopup {
id,
parent,
config,
widget_fn: Box::new(move || Box::new(widget_fn())),
});
PopupHandle { id }
}
pub fn surface_handle(id: SurfaceId) -> SurfaceHandle {
SurfaceHandle { id }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_measure_holds_fixed_axes_at_their_value_and_owned_ones_at_the_compositors() {
let confirmed = (300, 32);
let free = SurfaceConfig::new().width(content()).height(48);
assert_eq!(measure_pins(&free, confirmed), (None, Some(48)));
let stretched = SurfaceConfig::new()
.width(200)
.height(content())
.anchor(Anchor::TOP | Anchor::LEFT | Anchor::RIGHT);
assert_eq!(measure_pins(&stretched, confirmed), (Some(300), None));
}
#[test]
fn a_margin_converts_exactly_as_a_padding_does() {
let same = |m: Margin, p: crate::widgets::Padding, what: &str| {
assert_eq!(m.top as f32, p.top, "{what}: top");
assert_eq!(m.right as f32, p.right, "{what}: right");
assert_eq!(m.bottom as f32, p.bottom, "{what}: bottom");
assert_eq!(m.left as f32, p.left, "{what}: left");
};
same(
Margin::from(8),
crate::widgets::Padding::from(8),
"one value",
);
same(
Margin::all(8),
crate::widgets::Padding::all(8.0),
"all(), the named form",
);
same(
Margin::from([4, 12]),
crate::widgets::Padding::from([4, 12]),
"[vertical, horizontal]",
);
same(
Margin::from([1, 2, 3, 4]),
crate::widgets::Padding::from([1, 2, 3, 4]),
"[top, right, bottom, left]",
);
assert_eq!(
Margin::from([1, 2, 3, 4]),
Margin {
top: 1,
right: 2,
bottom: 3,
left: 4
}
);
assert_eq!(
Margin::from([4, 12]),
Margin {
top: 4,
right: 12,
bottom: 4,
left: 12
}
);
}
#[test]
fn an_auto_zone_adds_the_margin_facing_away_from_its_anchor() {
let margin = Margin::from([6, 20, 9, 30]);
let top = ExclusiveZone::Auto.resolve(Anchor::TOP, margin, 800, 32);
assert_eq!(top, 32 + 9, "a top bar counts its bottom margin");
let bottom = ExclusiveZone::Auto.resolve(Anchor::BOTTOM, margin, 800, 32);
assert_eq!(bottom, 32 + 6, "a bottom bar counts its top margin");
let left = ExclusiveZone::Auto.resolve(Anchor::LEFT, margin, 48, 600);
assert_eq!(left, 48 + 20, "a left dock counts its right margin");
let right = ExclusiveZone::Auto.resolve(Anchor::RIGHT, margin, 48, 600);
assert_eq!(right, 48 + 30, "a right dock counts its left margin");
let full_width = ExclusiveZone::Auto.resolve(
Anchor::TOP | Anchor::LEFT | Anchor::RIGHT,
margin,
800,
32,
);
assert_eq!(
full_width,
32 + 9,
"and a full-width bar the same as a top one"
);
}
#[test]
fn a_corner_or_full_anchor_has_no_axis_to_follow_and_reserves_nothing() {
let margin = Margin::all(10);
assert_eq!(
ExclusiveZone::Auto.resolve(Anchor::TOP | Anchor::LEFT, margin, 48, 600),
0
);
assert_eq!(
ExclusiveZone::Auto.resolve(
Anchor::TOP | Anchor::LEFT | Anchor::RIGHT | Anchor::BOTTOM,
margin,
48,
600
),
0,
"and so does a surface pinned to all four"
);
}
#[test]
fn the_other_zone_policies_ignore_the_surface() {
let m = Margin::all(10);
assert_eq!(ExclusiveZone::None.resolve(Anchor::TOP, m, 800, 32), 0);
assert_eq!(ExclusiveZone::Ignore.resolve(Anchor::TOP, m, 800, 32), -1);
assert_eq!(
ExclusiveZone::from(34u32).resolve(Anchor::TOP, m, 800, 32),
34
);
}
fn fresh() {
crate::reactive::owner::create_root_owner();
crate::app_state::reset();
}
fn popup() -> PopupHandle {
spawn_popup(SurfaceId::next(), PopupConfig::new(120), || {
crate::widgets::container()
})
}
#[test]
fn a_popup_reports_its_dismissal_after_the_scope_that_opened_it_is_gone() {
fresh();
let (child, parent_scope) = crate::reactive::owner::with_owner(|| {
let child = popup();
assert!(!child.dismissed());
child
});
crate::reactive::owner::dispose_owner_now(parent_scope);
assert!(!child.dismissed(), "a live popup is not dismissed");
mark_popup_dismissed(child.id());
assert!(child.dismissed(), "and its death is still readable");
}
#[test]
fn watching_a_popup_reacts_to_its_dismissal() {
fresh();
let popup = popup();
let seen = std::rc::Rc::new(std::cell::Cell::new(false));
let observer = seen.clone();
crate::reactive::create_effect(move || observer.set(popup.dismissed()));
assert!(!seen.get(), "the effect ran once, on a live popup");
mark_popup_dismissed(popup.id());
assert!(seen.get(), "the effect re-ran when the popup was dismissed");
}
#[test]
fn a_dismissal_speaks_only_for_the_popup_it_names() {
fresh();
let (first, second) = (popup(), popup());
mark_popup_dismissed(first.id());
assert!(first.dismissed());
assert!(!second.dismissed(), "the other popup is still open");
}
#[test]
fn a_watcher_stops_being_woken_once_its_own_popup_is_gone() {
fresh();
let (watched, other) = (popup(), popup());
let runs = std::rc::Rc::new(std::cell::Cell::new(0));
let counter = runs.clone();
crate::reactive::create_effect(move || {
watched.dismissed();
counter.set(counter.get() + 1);
});
assert_eq!(runs.get(), 1, "the initial run");
mark_popup_dismissed(watched.id());
assert_eq!(runs.get(), 2, "woken for its own popup");
mark_popup_dismissed(other.id());
assert_eq!(
runs.get(),
2,
"and not for anybody else's, once its own answer can no longer change"
);
}
#[test]
fn closing_leaves_the_announcement_to_the_close_that_follows() {
fresh();
let popup = popup();
popup.close();
assert!(!popup.dismissed(), "still open until the loop closes it");
let queued = drain_surface_commands();
assert!(
queued
.iter()
.any(|c| matches!(c, SurfaceCommand::Close(id) if *id == popup.id())),
"and the close is queued"
);
}
#[test]
fn a_scale_is_floored_at_one_and_a_repeat_of_it_is_not_a_change() {
assert_eq!(adopted_scale(1.0, 1.5), (1.5, true));
assert_eq!(adopted_scale(1.5, 1.5), (1.5, false));
assert_eq!(adopted_scale(1.5, 2.0), (2.0, true));
assert_eq!(adopted_scale(1.0, 1.0), (1.0, false));
assert_eq!(adopted_scale(2.0, 0.5), (1.0, true));
assert_eq!(adopted_scale(1.0, 0.5), (1.0, false));
}
#[test]
fn a_buffer_is_the_logical_size_scaled_and_rounded_half_away_from_zero() {
assert_eq!(buffer_size((2560, 33), 1.5), (3840, 50));
let truncated_scale = 1.5_f32 as u32;
assert_ne!(
(2560 * truncated_scale, 33 * truncated_scale),
buffer_size((2560, 33), 1.5)
);
assert_ne!(
((2560.0 * 1.5) as u32, (33.0 * 1.5) as u32),
buffer_size((2560, 33), 1.5)
);
assert_eq!(buffer_size((3840, 2160), 1.5), (5760, 3240));
assert_eq!(buffer_size((2560, 32), 1.0), (2560, 32));
assert_eq!(buffer_size((2560, 32), 2.0), (5120, 64));
assert_eq!(buffer_size((2560, 33), 125.0 / 120.0), (2667, 34));
}
}