1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
//! AspectRatio component for gpuikit
//!
//! A container component that maintains a specific aspect ratio for its content.
//! Useful for images, videos, and other media that need consistent proportions.
use gpui::{
AnyElement, App, IntoElement, ParentElement, Pixels, RenderOnce, Styled, Window, div,
prelude::FluentBuilder, relative,
};
/// Common aspect ratio presets
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum AspectRatioPreset {
/// 1:1 square ratio
Square,
/// 4:3 standard ratio (common for photos)
Standard,
/// 3:4 portrait ratio
Portrait,
/// 16:9 widescreen video ratio
Video,
/// 21:9 ultrawide ratio
Ultrawide,
/// 2:3 portrait photography ratio
Portrait23,
/// 3:2 landscape photography ratio
Landscape32,
}
impl AspectRatioPreset {
/// Get the numeric ratio value (width / height)
pub fn ratio(&self) -> f32 {
match self {
AspectRatioPreset::Square => 1.0,
AspectRatioPreset::Standard => 4.0 / 3.0,
AspectRatioPreset::Portrait => 3.0 / 4.0,
AspectRatioPreset::Video => 16.0 / 9.0,
AspectRatioPreset::Ultrawide => 21.0 / 9.0,
AspectRatioPreset::Portrait23 => 2.0 / 3.0,
AspectRatioPreset::Landscape32 => 3.0 / 2.0,
}
}
}
/// How the child content should fit within the aspect ratio container
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum ObjectFit {
/// Scale content to fill the container, maintaining aspect ratio.
/// Content may be clipped.
#[default]
Cover,
/// Scale content to fit entirely within the container, maintaining aspect ratio.
/// May result in letterboxing.
Contain,
/// Stretch content to fill the container exactly.
/// Aspect ratio of content may be distorted.
Fill,
/// Content is not resized. May overflow or leave empty space.
None,
}
/// A container that maintains a specific aspect ratio for its content.
#[derive(IntoElement)]
pub struct AspectRatio {
ratio: f32,
child: Option<AnyElement>,
object_fit: ObjectFit,
width: Option<Pixels>,
}
impl AspectRatio {
/// Create a new AspectRatio container with the given ratio (width / height).
pub fn new(ratio: f32) -> Self {
Self {
ratio: ratio.max(0.001), // Prevent division by zero
child: None,
object_fit: ObjectFit::default(),
width: None,
}
}
/// Create an AspectRatio from a preset ratio.
pub fn preset(preset: AspectRatioPreset) -> Self {
Self::new(preset.ratio())
}
/// Set the child element to display within the aspect ratio container.
pub fn child(mut self, child: impl IntoElement) -> Self {
self.child = Some(child.into_any_element());
self
}
/// Set how the child content should fit within the container.
pub fn object_fit(mut self, fit: ObjectFit) -> Self {
self.object_fit = fit;
self
}
/// Set a fixed width for the container. Height will be calculated from the aspect ratio.
pub fn width(mut self, width: impl Into<Pixels>) -> Self {
self.width = Some(width.into());
self
}
/// Set the aspect ratio (width / height).
pub fn ratio(mut self, ratio: f32) -> Self {
self.ratio = ratio.max(0.001);
self
}
}
impl Default for AspectRatio {
fn default() -> Self {
Self::new(1.0)
}
}
impl RenderOnce for AspectRatio {
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
// Use the padding-bottom technique to maintain aspect ratio
// This works by setting the height to 0 and using padding-bottom as a percentage of width
let padding_percent = (1.0 / self.ratio).clamp(0.0, 10.0);
let outer = div()
.relative()
.overflow_hidden()
.when_some(self.width, |el, w| el.w(w))
.when(self.width.is_none(), |el| el.w_full())
// Use padding-bottom to create the aspect ratio
.h(relative(0.0))
.pb(relative(padding_percent));
let inner = div()
.absolute()
.inset_0()
.flex()
.items_center()
.justify_center()
.when(matches!(self.object_fit, ObjectFit::Fill), |el| {
el.size_full()
})
.when(matches!(self.object_fit, ObjectFit::Cover), |el| {
el.size_full().overflow_hidden()
})
.when(matches!(self.object_fit, ObjectFit::Contain), |el| {
el.size_full()
})
.when(matches!(self.object_fit, ObjectFit::None), |el| el)
.when_some(self.child, |el, child| el.child(child));
outer.child(inner)
}
}
// Convenience functions
/// Create an AspectRatio container with a custom ratio (width / height).
///
/// # Example
/// ```
/// # use gpui::{Context, IntoElement, Render, Window, div, prelude::*};
/// use gpuikit::elements::aspect_ratio::aspect_ratio;
/// # struct D;
/// # impl Render for D { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
/// # let video_thumbnail = div();
/// aspect_ratio(16.0 / 9.0)
/// .child(video_thumbnail)
/// # }}
/// # let mut tcx = gpui::TestAppContext::single();
/// # tcx.update(gpuikit::init);
/// # let _ = tcx.add_window_view(|_, _| D);
/// ```
pub fn aspect_ratio(ratio: f32) -> AspectRatio {
AspectRatio::new(ratio)
}
/// Create a 1:1 square aspect ratio container.
pub fn aspect_ratio_square() -> AspectRatio {
AspectRatio::preset(AspectRatioPreset::Square)
}
/// Create a 16:9 widescreen video aspect ratio container.
pub fn aspect_ratio_video() -> AspectRatio {
AspectRatio::preset(AspectRatioPreset::Video)
}
/// Create a 3:4 portrait aspect ratio container.
pub fn aspect_ratio_portrait() -> AspectRatio {
AspectRatio::preset(AspectRatioPreset::Portrait)
}
/// Create a 4:3 standard aspect ratio container.
pub fn aspect_ratio_standard() -> AspectRatio {
AspectRatio::preset(AspectRatioPreset::Standard)
}
/// Create a 21:9 ultrawide aspect ratio container.
pub fn aspect_ratio_ultrawide() -> AspectRatio {
AspectRatio::preset(AspectRatioPreset::Ultrawide)
}