flex/
flex.rs

1// Copyright 2020 The Druid Authors.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Demonstrates alignment of children in the flex container.
16//! This example showcases the full set of functionality of flex, giving you
17//! knobs to change all the parameters. 99% of the time you will want to
18//! hard-code these parameters, which will simplify your code considerably.
19
20// On Windows platform, don't show a console when opening the app.
21#![windows_subsystem = "windows"]
22
23use druid::text::ParseFormatter;
24use druid::widget::prelude::*;
25use druid::widget::{
26    Button, Checkbox, CrossAxisAlignment, Flex, Label, MainAxisAlignment, ProgressBar, RadioGroup,
27    SizedBox, Slider, Stepper, Switch, TextBox, WidgetExt,
28};
29use druid::{AppLauncher, Color, Data, Lens, WidgetId, WindowDesc};
30
31const DEFAULT_SPACER_SIZE: f64 = 8.;
32const SPACER_OPTIONS: [(&str, Spacers); 4] = [
33    ("None", Spacers::None),
34    ("Default", Spacers::Default),
35    ("Flex", Spacers::Flex),
36    ("Fixed:", Spacers::Fixed),
37];
38const MAIN_AXIS_ALIGNMENT_OPTIONS: [(&str, MainAxisAlignment); 6] = [
39    ("Start", MainAxisAlignment::Start),
40    ("Center", MainAxisAlignment::Center),
41    ("End", MainAxisAlignment::End),
42    ("Between", MainAxisAlignment::SpaceBetween),
43    ("Evenly", MainAxisAlignment::SpaceEvenly),
44    ("Around", MainAxisAlignment::SpaceAround),
45];
46const CROSS_AXIS_ALIGNMENT_OPTIONS: [(&str, CrossAxisAlignment); 5] = [
47    ("Start", CrossAxisAlignment::Start),
48    ("Center", CrossAxisAlignment::Center),
49    ("End", CrossAxisAlignment::End),
50    ("Baseline", CrossAxisAlignment::Baseline),
51    ("Fill", CrossAxisAlignment::Fill),
52];
53const FLEX_TYPE_OPTIONS: [(&str, FlexType); 2] =
54    [("Row", FlexType::Row), ("Column", FlexType::Column)];
55
56#[derive(Clone, Data, Lens)]
57struct AppState {
58    demo_state: DemoState,
59    params: Params,
60}
61
62#[derive(Clone, Data, Lens)]
63struct DemoState {
64    pub input_text: String,
65    pub enabled: bool,
66    volume: f64,
67}
68
69#[derive(Clone, Data, Lens)]
70struct Params {
71    axis: FlexType,
72    cross_alignment: CrossAxisAlignment,
73    main_alignment: MainAxisAlignment,
74    fill_major_axis: bool,
75    debug_layout: bool,
76    fix_minor_axis: bool,
77    fix_major_axis: bool,
78    spacers: Spacers,
79    spacer_size: f64,
80}
81
82#[derive(Clone, Copy, PartialEq, Data)]
83enum Spacers {
84    None,
85    Default,
86    Flex,
87    Fixed,
88}
89
90#[derive(Clone, Copy, PartialEq, Data)]
91enum FlexType {
92    Row,
93    Column,
94}
95
96/// builds a child Flex widget from some parameters.
97struct Rebuilder {
98    inner: Box<dyn Widget<AppState>>,
99}
100
101impl Rebuilder {
102    fn new() -> Rebuilder {
103        Rebuilder {
104            inner: SizedBox::empty().boxed(),
105        }
106    }
107
108    fn rebuild_inner(&mut self, data: &AppState) {
109        self.inner = build_widget(&data.params);
110    }
111}
112
113impl Widget<AppState> for Rebuilder {
114    fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut AppState, env: &Env) {
115        self.inner.event(ctx, event, data, env)
116    }
117
118    fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &AppState, env: &Env) {
119        if let LifeCycle::WidgetAdded = event {
120            self.rebuild_inner(data);
121        }
122        self.inner.lifecycle(ctx, event, data, env)
123    }
124
125    fn update(&mut self, ctx: &mut UpdateCtx, old_data: &AppState, data: &AppState, env: &Env) {
126        if !old_data.params.same(&data.params) {
127            self.rebuild_inner(data);
128            ctx.children_changed();
129        } else {
130            self.inner.update(ctx, old_data, data, env);
131        }
132    }
133
134    fn layout(
135        &mut self,
136        ctx: &mut LayoutCtx,
137        bc: &BoxConstraints,
138        data: &AppState,
139        env: &Env,
140    ) -> Size {
141        self.inner.layout(ctx, bc, data, env)
142    }
143
144    fn paint(&mut self, ctx: &mut PaintCtx, data: &AppState, env: &Env) {
145        self.inner.paint(ctx, data, env)
146    }
147
148    fn id(&self) -> Option<WidgetId> {
149        self.inner.id()
150    }
151}
152
153fn make_control_row() -> impl Widget<AppState> {
154    Flex::row()
155        .cross_axis_alignment(CrossAxisAlignment::Start)
156        .with_child(
157            Flex::column()
158                .cross_axis_alignment(CrossAxisAlignment::Start)
159                .with_child(Label::new("Type:"))
160                .with_default_spacer()
161                .with_child(RadioGroup::column(FLEX_TYPE_OPTIONS.to_vec()).lens(Params::axis)),
162        )
163        .with_default_spacer()
164        .with_child(
165            Flex::column()
166                .cross_axis_alignment(CrossAxisAlignment::Start)
167                .with_child(Label::new("CrossAxis:"))
168                .with_default_spacer()
169                .with_child(
170                    RadioGroup::column(CROSS_AXIS_ALIGNMENT_OPTIONS.to_vec())
171                        .lens(Params::cross_alignment),
172                ),
173        )
174        .with_default_spacer()
175        .with_child(
176            Flex::column()
177                .cross_axis_alignment(CrossAxisAlignment::Start)
178                .with_child(Label::new("MainAxis:"))
179                .with_default_spacer()
180                .with_child(
181                    RadioGroup::column(MAIN_AXIS_ALIGNMENT_OPTIONS.to_vec())
182                        .lens(Params::main_alignment),
183                ),
184        )
185        .with_default_spacer()
186        .with_child(make_spacer_select())
187        .with_default_spacer()
188        .with_child(
189            Flex::column()
190                .cross_axis_alignment(CrossAxisAlignment::Start)
191                .with_child(Label::new("Misc:"))
192                .with_default_spacer()
193                .with_child(Checkbox::new("Debug layout").lens(Params::debug_layout))
194                .with_default_spacer()
195                .with_child(Checkbox::new("Fill main axis").lens(Params::fill_major_axis))
196                .with_default_spacer()
197                .with_child(Checkbox::new("Fix minor axis size").lens(Params::fix_minor_axis))
198                .with_default_spacer()
199                .with_child(Checkbox::new("Fix major axis size").lens(Params::fix_major_axis)),
200        )
201        .padding(10.0)
202        .border(Color::grey(0.6), 2.0)
203        .rounded(5.0)
204        .lens(AppState::params)
205}
206
207fn make_spacer_select() -> impl Widget<Params> {
208    Flex::column()
209        .cross_axis_alignment(CrossAxisAlignment::Start)
210        .with_child(Label::new("Insert Spacers:"))
211        .with_default_spacer()
212        .with_child(RadioGroup::column(SPACER_OPTIONS.to_vec()).lens(Params::spacers))
213        .with_default_spacer()
214        .with_child(
215            Flex::row()
216                .with_child(
217                    TextBox::new()
218                        .with_formatter(ParseFormatter::new())
219                        .lens(Params::spacer_size)
220                        .fix_width(60.0),
221                )
222                .with_spacer(druid::theme::WIDGET_CONTROL_COMPONENT_PADDING)
223                .with_child(
224                    Stepper::new()
225                        .with_range(2.0, 50.0)
226                        .with_step(2.0)
227                        .lens(Params::spacer_size),
228                ),
229        )
230}
231
232fn space_if_needed<T: Data>(flex: &mut Flex<T>, params: &Params) {
233    match params.spacers {
234        Spacers::None => (),
235        Spacers::Default => flex.add_default_spacer(),
236        Spacers::Fixed => flex.add_spacer(params.spacer_size),
237        Spacers::Flex => flex.add_flex_spacer(1.0),
238    }
239}
240
241fn build_widget(state: &Params) -> Box<dyn Widget<AppState>> {
242    let mut flex = match state.axis {
243        FlexType::Column => Flex::column(),
244        FlexType::Row => Flex::row(),
245    }
246    .cross_axis_alignment(state.cross_alignment)
247    .main_axis_alignment(state.main_alignment)
248    .must_fill_main_axis(state.fill_major_axis);
249
250    flex.add_child(
251        TextBox::new()
252            .with_placeholder("Sample text")
253            .lens(DemoState::input_text),
254    );
255    space_if_needed(&mut flex, state);
256
257    flex.add_child(
258        Button::new("Clear").on_click(|_ctx, data: &mut DemoState, _env| {
259            data.input_text.clear();
260            data.enabled = false;
261            data.volume = 0.0;
262        }),
263    );
264
265    space_if_needed(&mut flex, state);
266
267    flex.add_child(
268        Label::new(|data: &DemoState, _: &Env| data.input_text.clone()).with_text_size(32.0),
269    );
270    space_if_needed(&mut flex, state);
271    flex.add_child(Checkbox::new("Demo").lens(DemoState::enabled));
272    space_if_needed(&mut flex, state);
273    flex.add_child(Switch::new().lens(DemoState::enabled));
274    space_if_needed(&mut flex, state);
275    flex.add_child(Slider::new().lens(DemoState::volume));
276    space_if_needed(&mut flex, state);
277    flex.add_child(ProgressBar::new().lens(DemoState::volume));
278    space_if_needed(&mut flex, state);
279    flex.add_child(
280        Stepper::new()
281            .with_range(0.0, 1.0)
282            .with_step(0.1)
283            .with_wraparound(true)
284            .lens(DemoState::volume),
285    );
286
287    let mut flex = SizedBox::new(flex);
288    if state.fix_minor_axis {
289        match state.axis {
290            FlexType::Row => flex = flex.height(200.),
291            FlexType::Column => flex = flex.width(200.),
292        }
293    }
294    if state.fix_major_axis {
295        match state.axis {
296            FlexType::Row => flex = flex.width(600.),
297            FlexType::Column => flex = flex.height(300.),
298        }
299    }
300
301    let flex = flex
302        .padding(8.0)
303        .border(Color::grey(0.6), 2.0)
304        .rounded(5.0)
305        .lens(AppState::demo_state);
306
307    if state.debug_layout {
308        flex.debug_paint_layout().boxed()
309    } else {
310        flex.boxed()
311    }
312}
313
314fn make_ui() -> impl Widget<AppState> {
315    Flex::column()
316        .must_fill_main_axis(true)
317        .with_child(make_control_row())
318        .with_default_spacer()
319        .with_flex_child(Rebuilder::new().center(), 1.0)
320        .padding(10.0)
321}
322
323pub fn main() {
324    let main_window = WindowDesc::new(make_ui())
325        .window_size((720., 600.))
326        .with_min_size((620., 300.))
327        .title("Flex Container Options");
328
329    let demo_state = DemoState {
330        input_text: "hello".into(),
331        enabled: false,
332        volume: 0.0,
333    };
334
335    let params = Params {
336        axis: FlexType::Row,
337        cross_alignment: CrossAxisAlignment::Center,
338        main_alignment: MainAxisAlignment::Start,
339        debug_layout: false,
340        fix_minor_axis: false,
341        fix_major_axis: false,
342        spacers: Spacers::None,
343        spacer_size: DEFAULT_SPACER_SIZE,
344        fill_major_axis: false,
345    };
346
347    AppLauncher::with_window(main_window)
348        .log_to_console()
349        .launch(AppState { demo_state, params })
350        .expect("Failed to launch application");
351}