use gpui::{
div, px, relative, size, AnyElement, App, AvailableSpace, Bounds, Element, ElementId,
GlobalElementId, InspectorElementId, IntoElement, LayoutId, ParentElement, Pixels, RenderOnce,
Size, Style, Styled, Window,
};
use crate::RhythmGrid;
#[derive(IntoElement)]
pub struct RhythmFrame {
grid: RhythmGrid,
ratio: f32,
fit: RhythmFit,
children: Vec<AnyElement>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum RhythmFit {
#[default]
Pad,
Crop,
}
pub fn rhythm_frame(grid: RhythmGrid, ratio: f32) -> RhythmFrame {
assert!(
ratio.is_finite() && ratio > 0.0,
"aspect ratio must be finite and greater than zero"
);
RhythmFrame {
grid,
ratio,
fit: RhythmFit::Pad,
children: Vec::new(),
}
}
impl RhythmFrame {
#[must_use]
pub fn fit(mut self, fit: RhythmFit) -> Self {
self.fit = fit;
self
}
#[must_use]
pub fn crop(self) -> Self {
self.fit(RhythmFit::Crop)
}
}
impl ParentElement for RhythmFrame {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements)
}
}
impl RenderOnce for RhythmFrame {
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
let mut natural = div().w_full().flex_none();
natural.style().aspect_ratio = Some(self.ratio);
let natural = natural.children(self.children);
let mut mask = div().absolute().inset_0().overflow_hidden();
if self.fit == RhythmFit::Crop {
mask = mask.flex().flex_row().items_center();
}
div()
.relative()
.w_full()
.child(FrameSizer {
grid: self.grid,
ratio: self.ratio,
fit: self.fit,
})
.child(mask.child(natural))
}
}
struct FrameSizer {
grid: RhythmGrid,
ratio: f32,
fit: RhythmFit,
}
fn snapped_height(grid: RhythmGrid, ratio: f32, fit: RhythmFit, width: Pixels) -> Pixels {
let natural = px(f32::from(width) / ratio);
match fit {
RhythmFit::Pad => grid.snap_up(natural),
RhythmFit::Crop => grid.snap_down(natural),
}
}
impl IntoElement for FrameSizer {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
impl Element for FrameSizer {
type RequestLayoutState = ();
type PrepaintState = ();
fn id(&self) -> Option<ElementId> {
None
}
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
window: &mut Window,
_cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
let grid = self.grid;
let ratio = self.ratio;
let fit = self.fit;
let mut style = Style::default();
style.size.width = relative(1.).into();
let layout_id = window.request_measured_layout(style, move |known, available, _, _| {
let width = known.width.or(match available.width {
AvailableSpace::Definite(width) => Some(width),
AvailableSpace::MinContent | AvailableSpace::MaxContent => None,
});
match width {
Some(width) => size(width, snapped_height(grid, ratio, fit, width)),
None => Size::default(),
}
});
(layout_id, ())
}
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_bounds: Bounds<Pixels>,
_request_layout: &mut Self::RequestLayoutState,
_window: &mut Window,
_cx: &mut App,
) -> Self::PrepaintState {
}
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_bounds: Bounds<Pixels>,
_request_layout: &mut Self::RequestLayoutState,
_prepaint: &mut Self::PrepaintState,
_window: &mut Window,
_cx: &mut App,
) {
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn snapped_height_pads_up_and_crops_down() {
let grid = RhythmGrid::new(px(8.0));
assert_eq!(
snapped_height(grid, 16. / 9., RhythmFit::Pad, px(800.0)),
px(456.0)
);
assert_eq!(
snapped_height(grid, 16. / 9., RhythmFit::Crop, px(800.0)),
px(448.0)
);
assert_eq!(
snapped_height(grid, 2.0, RhythmFit::Pad, px(96.0)),
px(48.0)
);
assert_eq!(
snapped_height(grid, 2.0, RhythmFit::Crop, px(96.0)),
px(48.0)
);
}
#[test]
fn frame_collects_children_and_the_fit_mode() {
let grid = RhythmGrid::new(px(8.0));
let mut frame = rhythm_frame(grid, 16. / 9.);
assert_eq!(frame.fit, RhythmFit::Pad);
frame.extend([gpui::Empty.into_any_element()]);
let frame = frame.crop();
assert_eq!(frame.fit, RhythmFit::Crop);
assert_eq!(frame.children.len(), 1);
assert_eq!(rhythm_frame(grid, 2.0).crop().fit, RhythmFit::Crop);
assert_eq!(
rhythm_frame(grid, 2.0).fit(RhythmFit::Pad).fit,
RhythmFit::Pad
);
}
#[test]
#[should_panic(expected = "aspect ratio must be finite and greater than zero")]
fn frame_rejects_a_non_positive_ratio() {
let _ = rhythm_frame(RhythmGrid::new(px(8.0)), 0.0);
}
#[cfg(feature = "test-support")]
#[gpui::test]
fn frame_layout_pads_at_the_bottom_and_crops_both_edges(cx: &mut gpui::TestAppContext) {
use gpui::{point, AvailableSpace, InteractiveElement};
let cx = cx.add_empty_window();
let grid = RhythmGrid::new(px(8.0));
cx.draw(
point(px(0.0), px(0.0)),
size(
AvailableSpace::Definite(px(800.0)),
AvailableSpace::MaxContent,
),
|_, _| {
div()
.w(px(800.0))
.flex()
.flex_col()
.child(
div()
.flex_none()
.debug_selector(|| "pad-frame".into())
.child(
rhythm_frame(grid, 16. / 9.).child(
div().size_full().debug_selector(|| "pad-content".into()),
),
),
)
.child(
div()
.flex_none()
.debug_selector(|| "crop-frame".into())
.child(
rhythm_frame(grid, 16. / 9.).crop().child(
div().size_full().debug_selector(|| "crop-content".into()),
),
),
)
},
);
let pad_frame = cx.debug_bounds("pad-frame").expect("pad frame bounds");
let pad_content = cx.debug_bounds("pad-content").expect("pad content bounds");
assert_eq!(pad_frame.size.height, px(456.0));
assert_eq!(pad_content.size.height, px(450.0));
assert_eq!(pad_content.origin.y, pad_frame.origin.y);
let crop_frame = cx.debug_bounds("crop-frame").expect("crop frame bounds");
let crop_content = cx
.debug_bounds("crop-content")
.expect("crop content bounds");
assert_eq!(crop_frame.size.height, px(448.0));
assert_eq!(crop_content.size.height, px(450.0));
let top_overflow = crop_frame.origin.y - crop_content.origin.y;
let bottom_overflow = (crop_content.origin.y + crop_content.size.height)
- (crop_frame.origin.y + crop_frame.size.height);
assert_eq!(top_overflow, px(1.0));
assert_eq!(bottom_overflow, px(1.0));
}
}