Skip to main content

material_ui_rs/widget/component/
page.rs

1//! Material page layout helpers.
2
3use iced_widget::core::text as core_text;
4use iced_widget::core::{Element, Length, alignment};
5use iced_widget::text;
6use iced_widget::{Column, Container, Row, Scrollable, Text};
7
8use crate::{Theme, text as material_text, tokens};
9
10/// Maximum content width used by Material page surfaces.
11pub const MAX_WIDTH: f32 = 980.0;
12
13/// Outer page padding used by Material page surfaces.
14pub const PADDING: f32 = 28.0;
15
16/// Vertical spacing between a page header and body.
17pub const SPACING: f32 = 28.0;
18
19/// Vertical spacing between page sections.
20pub const SECTION_SPACING: f32 = 24.0;
21
22/// Default spacing for grouped page content.
23pub const STACK_SPACING: f32 = 16.0;
24
25/// Compact spacing for dense vertical page content.
26pub const COMPACT_STACK_SPACING: f32 = 8.0;
27
28/// Spacing for repeated component previews.
29pub const COMPONENT_STACK_SPACING: f32 = 12.0;
30
31/// Dense spacing for related controls that need a little separation.
32pub const DENSE_STACK_SPACING: f32 = 10.0;
33
34/// Spacious spacing for separated control groups.
35pub const SPACIOUS_STACK_SPACING: f32 = 18.0;
36
37/// Default spacing for horizontal page actions.
38pub const ROW_SPACING: f32 = 12.0;
39
40/// Compact spacing for dense horizontal page actions.
41pub const COMPACT_ROW_SPACING: f32 = 8.0;
42
43/// Spacing for paired indicator previews.
44pub const INDICATOR_ROW_SPACING: f32 = 16.0;
45
46/// Height used by compact divider demonstration rows.
47pub const DIVIDER_ROW_HEIGHT: f32 = 32.0;
48
49/// Spacing used by compact divider demonstration rows.
50pub const DIVIDER_ROW_SPACING: f32 = 16.0;
51
52/// Padding used by compact showcase cards.
53pub const CARD_PADDING: f32 = 12.0;
54
55/// Height used by compact showcase cards.
56pub const CARD_HEIGHT: f32 = 78.0;
57
58/// Maximum width used by component previews.
59pub const PREVIEW_MAX_WIDTH: f32 = tokens::component::bottom_sheet::SHEET_MAX_WIDTH;
60
61/// Horizontal space reserved around component previews before clamping.
62pub const PREVIEW_HORIZONTAL_RESERVE: f32 = 160.0;
63
64/// Minimum width used by component previews.
65pub const PREVIEW_MIN_WIDTH: f32 = 320.0;
66
67/// Height used by fixed component preview panes.
68pub const PREVIEW_HEIGHT: f32 = 260.0;
69
70/// Creates a scrollable Material page surface.
71pub fn surface<'a, Message, Renderer>(
72    header: impl Into<Element<'a, Message, Theme, Renderer>>,
73    body: impl Into<Element<'a, Message, Theme, Renderer>>,
74) -> Scrollable<'a, Message, Theme, Renderer>
75where
76    Message: 'a,
77    Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
78{
79    let page = Column::new()
80        .push(header)
81        .push(body)
82        .spacing(SPACING)
83        .padding(PADDING)
84        .width(Length::Fill)
85        .max_width(MAX_WIDTH);
86
87    Scrollable::new(
88        super::container::surface_container_high(page)
89            .width(Length::Fill)
90            .center_x(Length::Fill),
91    )
92    .height(Length::Fill)
93}
94
95/// Computes a responsive width for component previews.
96pub fn preview_width(viewport_width: f32) -> f32 {
97    (viewport_width - PREVIEW_HORIZONTAL_RESERVE).clamp(PREVIEW_MIN_WIDTH, PREVIEW_MAX_WIDTH)
98}
99
100/// Centers preview content at a fixed Material preview width.
101pub fn centered_preview<'a, Message, Renderer>(
102    width: f32,
103    content: impl Into<Element<'a, Message, Theme, Renderer>>,
104) -> Container<'a, Message, Theme, Renderer>
105where
106    Message: 'a,
107    Renderer: iced_widget::core::Renderer + 'a,
108{
109    Container::new(Container::new(content).width(Length::Fixed(width)))
110        .width(Length::Fill)
111        .align_x(alignment::Horizontal::Center)
112}
113
114/// Creates a fixed-height component preview pane.
115pub fn preview_pane<'a, Message, Renderer>(
116    content: impl Into<Element<'a, Message, Theme, Renderer>>,
117) -> Container<'a, Message, Theme, Renderer>
118where
119    Message: 'a,
120    Renderer: iced_widget::core::Renderer + 'a,
121{
122    Container::new(content)
123        .width(Length::Fill)
124        .height(Length::Fixed(PREVIEW_HEIGHT))
125}
126
127/// Creates a fixed-height component preview pane with horizontal alignment.
128pub fn aligned_preview_pane<'a, Message, Renderer>(
129    alignment: alignment::Horizontal,
130    content: impl Into<Element<'a, Message, Theme, Renderer>>,
131) -> Container<'a, Message, Theme, Renderer>
132where
133    Message: 'a,
134    Renderer: iced_widget::core::Renderer + 'a,
135{
136    preview_pane(content).align_x(alignment)
137}
138
139/// Creates a simple Material page header.
140pub fn header<'a, Message, Renderer>(
141    title: impl text::IntoFragment<'a>,
142    subtitle: impl text::IntoFragment<'a>,
143) -> Column<'a, Message, Theme, Renderer>
144where
145    Message: 'a,
146    Renderer: core_text::Renderer + 'a,
147{
148    Column::new()
149        .push(type_scale_text(title, tokens::typography::HEADLINE_LARGE))
150        .push(type_scale_text(subtitle, tokens::typography::BODY_LARGE))
151        .spacing(6)
152}
153
154/// Creates a titled Material page section.
155pub fn section<'a, Message, Renderer>(
156    title: impl text::IntoFragment<'a>,
157    body: impl Into<Element<'a, Message, Theme, Renderer>>,
158) -> Column<'a, Message, Theme, Renderer>
159where
160    Message: 'a,
161    Renderer: core_text::Renderer + 'a,
162{
163    Column::new()
164        .push(type_scale_text(title, tokens::typography::TITLE_MEDIUM))
165        .push(body)
166        .spacing(12)
167        .width(Length::Fill)
168}
169
170/// Creates a full-width page section list with dividers between sections.
171pub fn sections<'a, Message, Renderer>(
172    sections: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
173) -> Column<'a, Message, Theme, Renderer>
174where
175    Message: 'a,
176    Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
177{
178    let mut content = Column::new().spacing(SECTION_SPACING).width(Length::Fill);
179    let mut first = true;
180
181    for section in sections {
182        if first {
183            first = false;
184        } else {
185            content = content.push(super::rule::horizontal_inset());
186        }
187
188        content = content.push(section);
189    }
190
191    content
192}
193
194/// Creates a full-width vertical stack for page content.
195pub fn stack<'a, Message, Renderer>(
196    children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
197) -> Column<'a, Message, Theme, Renderer>
198where
199    Message: 'a,
200    Renderer: iced_widget::core::Renderer + 'a,
201{
202    Column::with_children(children)
203        .spacing(STACK_SPACING)
204        .width(Length::Fill)
205}
206
207/// Creates a compact full-width vertical stack.
208pub fn compact_stack<'a, Message, Renderer>(
209    children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
210) -> Column<'a, Message, Theme, Renderer>
211where
212    Message: 'a,
213    Renderer: iced_widget::core::Renderer + 'a,
214{
215    stack(children).spacing(COMPACT_STACK_SPACING)
216}
217
218/// Creates a full-width stack for repeated component previews.
219pub fn component_stack<'a, Message, Renderer>(
220    children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
221) -> Column<'a, Message, Theme, Renderer>
222where
223    Message: 'a,
224    Renderer: iced_widget::core::Renderer + 'a,
225{
226    stack(children).spacing(COMPONENT_STACK_SPACING)
227}
228
229/// Creates a dense full-width vertical stack.
230pub fn dense_stack<'a, Message, Renderer>(
231    children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
232) -> Column<'a, Message, Theme, Renderer>
233where
234    Message: 'a,
235    Renderer: iced_widget::core::Renderer + 'a,
236{
237    stack(children).spacing(DENSE_STACK_SPACING)
238}
239
240/// Creates a spacious full-width vertical stack.
241pub fn spacious_stack<'a, Message, Renderer>(
242    children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
243) -> Column<'a, Message, Theme, Renderer>
244where
245    Message: 'a,
246    Renderer: iced_widget::core::Renderer + 'a,
247{
248    stack(children).spacing(SPACIOUS_STACK_SPACING)
249}
250
251/// Creates a centered row for page controls.
252pub fn row<'a, Message, Renderer>(
253    children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
254) -> Row<'a, Message, Theme, Renderer>
255where
256    Message: 'a,
257    Renderer: iced_widget::core::Renderer + 'a,
258{
259    Row::with_children(children)
260        .spacing(ROW_SPACING)
261        .align_y(alignment::Vertical::Center)
262}
263
264/// Creates a centered row for paired indicator previews.
265pub fn indicator_row<'a, Message, Renderer>(
266    children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
267) -> Row<'a, Message, Theme, Renderer>
268where
269    Message: 'a,
270    Renderer: iced_widget::core::Renderer + 'a,
271{
272    row(children).spacing(INDICATOR_ROW_SPACING)
273}
274
275/// Creates a compact centered row for dense controls like chips and badges.
276pub fn compact_row<'a, Message, Renderer>(
277    children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
278) -> Row<'a, Message, Theme, Renderer>
279where
280    Message: 'a,
281    Renderer: iced_widget::core::Renderer + 'a,
282{
283    Row::with_children(children)
284        .spacing(COMPACT_ROW_SPACING)
285        .align_y(alignment::Vertical::Center)
286}
287
288/// Creates a full-width row with a leading label and trailing value.
289pub fn labeled_value_row<'a, Message, Renderer>(
290    label: impl text::IntoFragment<'a>,
291    value: impl text::IntoFragment<'a>,
292) -> Row<'a, Message, Theme, Renderer>
293where
294    Message: 'a,
295    Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
296{
297    row([
298        type_scale_text(label, tokens::typography::BODY_LARGE)
299            .width(Length::Fill)
300            .into(),
301        type_scale_text(value, tokens::typography::BODY_LARGE).into(),
302    ])
303}
304
305/// Creates a compact row for showing vertical dividers.
306pub fn divider_row<'a, Message, Renderer>(
307    children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
308) -> Row<'a, Message, Theme, Renderer>
309where
310    Message: 'a,
311    Renderer: iced_widget::core::Renderer + 'a,
312{
313    row(children)
314        .height(Length::Fixed(DIVIDER_ROW_HEIGHT))
315        .spacing(DIVIDER_ROW_SPACING)
316}
317
318/// Creates a compact titled card using one of the Material card constructors.
319pub fn card<'a, Message, Renderer>(
320    style: fn(Element<'a, Message, Theme, Renderer>) -> Container<'a, Message, Theme, Renderer>,
321    title: impl text::IntoFragment<'a>,
322    subtitle: impl text::IntoFragment<'a>,
323) -> Container<'a, Message, Theme, Renderer>
324where
325    Message: 'a,
326    Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
327{
328    style(
329        stack([
330            type_scale_text(title, tokens::typography::TITLE_MEDIUM).into(),
331            type_scale_text(subtitle, tokens::typography::BODY_MEDIUM).into(),
332        ])
333        .spacing(2)
334        .into(),
335    )
336    .padding(CARD_PADDING)
337    .height(Length::Fixed(CARD_HEIGHT))
338    .width(Length::Fill)
339}
340
341/// Creates text using a Material type scale.
342pub fn type_scale_text<'a, Renderer>(
343    content: impl text::IntoFragment<'a>,
344    scale: tokens::typography::TypeScale,
345) -> Text<'a, Theme, Renderer>
346where
347    Renderer: core_text::Renderer + 'a,
348{
349    material_text::type_scale(content, scale)
350}
351
352#[cfg(test)]
353#[path = "../../../tests/widget/component/page.rs"]
354mod tests;