druid 0.8.3

Data-oriented Rust UI design toolkit.
Documentation
// Copyright 2020 The xi-editor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! An example showcasing various widgets.

// On Windows platform, don't show a console when opening the app.
#![windows_subsystem = "windows"]

use druid::{
    im,
    kurbo::{Affine, BezPath, Circle, Point},
    piet::{FixedLinearGradient, GradientStop, InterpolationMode},
    widget::{
        prelude::*, Button, Checkbox, FillStrat, Flex, Image, Label, List, Painter, ProgressBar,
        RadioGroup, Scroll, Slider, Spinner, Stepper, Switch, TextBox,
    },
    AppLauncher, Color, Data, ImageBuf, Lens, Widget, WidgetExt, WidgetPod, WindowDesc,
};

#[cfg(feature = "svg")]
use druid::widget::{Svg, SvgData};

const XI_IMAGE: &[u8] = include_bytes!("assets/xi.image");

#[derive(Clone, Data, Lens)]
struct AppData {
    label_data: String,
    checkbox_data: bool,
    clicked_count: u64,
    list_items: im::Vector<String>,
    progressbar: f64,
    radio: MyRadio,
    stepper: f64,
    editable_text: String,
}

#[derive(Clone, Data, PartialEq)]
enum MyRadio {
    GaGa,
    GuGu,
    BaaBaa,
}

pub fn main() {
    let main_window = WindowDesc::new(ui_builder()).title("Widget Gallery");
    // Set our initial data
    let data = AppData {
        label_data: "test".into(),
        checkbox_data: false,
        clicked_count: 0,
        list_items: im::vector!["1".into(), "2".into(), "3".into()],
        progressbar: 0.5,
        radio: MyRadio::GaGa,
        stepper: 0.0,
        editable_text: "edit me!".into(),
    };
    AppLauncher::with_window(main_window)
        .log_to_console()
        .launch(data)
        .expect("launch failed");
}

fn ui_builder() -> impl Widget<AppData> {
    #[cfg(feature = "svg")]
    let svg_example = label_widget(
        Svg::new(
            include_str!("./assets/tiger.svg")
                .parse::<SvgData>()
                .unwrap(),
        ),
        "Svg",
    );

    #[cfg(not(feature = "svg"))]
    let svg_example = label_widget(Label::new("SVG not supported (yet)").center(), "Svg");

    Scroll::new(
        SquaresGrid::new()
            .with_cell_size(Size::new(200.0, 240.0))
            .with_spacing(20.0)
            .with_child(label_widget(
                Label::new(|data: &AppData, _: &_| data.label_data.clone()),
                "Label",
            ))
            .with_child(label_widget(
                Flex::column()
                    .with_child(
                        Button::new("Click me!")
                            .on_click(|_, data: &mut AppData, _: &_| data.clicked_count += 1),
                    )
                    .with_spacer(4.0)
                    .with_child(Label::new(|data: &AppData, _: &_| {
                        format!("Clicked {} times!", data.clicked_count)
                    })),
                "Button",
            ))
            .with_child(label_widget(
                Checkbox::new("Check me!").lens(AppData::checkbox_data),
                "Checkbox",
            ))
            .with_child(label_widget(
                List::new(|| {
                    Label::new(|data: &String, _: &_| format!("List item: {}", data))
                        .center()
                        .background(Color::hlc(230.0, 50.0, 50.0))
                        .fix_height(40.0)
                        .expand_width()
                })
                .lens(AppData::list_items),
                "List",
            ))
            .with_child(label_widget(
                Flex::column()
                    .with_child(ProgressBar::new().lens(AppData::progressbar))
                    .with_spacer(4.0)
                    .with_child(Label::new(|data: &AppData, _: &_| {
                        format!("{:.1}%", data.progressbar * 100.0)
                    }))
                    .with_spacer(4.0)
                    .with_child(
                        Flex::row()
                            .with_child(Button::new("<<").on_click(|_, data: &mut AppData, _| {
                                data.progressbar = (data.progressbar - 0.05).max(0.0);
                            }))
                            .with_spacer(4.0)
                            .with_child(Button::new(">>").on_click(|_, data: &mut AppData, _| {
                                data.progressbar = (data.progressbar + 0.05).min(1.0);
                            })),
                    ),
                "ProgressBar",
            ))
            // The image example here uses hard-coded literal image data included in the binary.
            // You may also want to load an image at runtime using a crate like `image`.
            .with_child(label_widget(
                Painter::new(paint_example).fix_size(32.0, 32.0),
                "Painter",
            ))
            .with_child(label_widget(
                RadioGroup::column(vec![
                    ("radio gaga", MyRadio::GaGa),
                    ("radio gugu", MyRadio::GuGu),
                    ("radio baabaa", MyRadio::BaaBaa),
                ])
                .lens(AppData::radio),
                "RadioGroup",
            ))
            .with_child(label_widget(
                Flex::column()
                    .with_child(
                        Slider::new()
                            .with_range(0.05, 0.95)
                            .with_step(0.10)
                            .lens(AppData::progressbar),
                    )
                    .with_spacer(4.0)
                    .with_child(Label::new(|data: &AppData, _: &_| {
                        format!("{:3.2}%", data.progressbar * 100.)
                    })),
                "Slider",
            ))
            .with_child(label_widget(
                Flex::row()
                    .with_child(Stepper::new().lens(AppData::stepper))
                    .with_spacer(4.0)
                    .with_child(Label::new(|data: &AppData, _: &_| {
                        format!("{:.1}", data.stepper)
                    })),
                "Stepper",
            ))
            .with_child(label_widget(
                TextBox::new().lens(AppData::editable_text),
                "TextBox",
            ))
            .with_child(label_widget(
                Switch::new().lens(AppData::checkbox_data),
                "Switch",
            ))
            .with_child(label_widget(
                Spinner::new().fix_height(40.0).center(),
                "Spinner",
            ))
            .with_child(label_widget(
                Image::new(
                    ImageBuf::from_data(include_bytes!("./assets/PicWithAlpha.png")).unwrap(),
                )
                .fill_mode(FillStrat::Fill)
                .interpolation_mode(InterpolationMode::Bilinear),
                "Image",
            ))
            .with_child(svg_example),
    )
    .vertical()
}

fn label_widget<T: Data>(widget: impl Widget<T> + 'static, label: &str) -> impl Widget<T> {
    Flex::column()
        .must_fill_main_axis(true)
        .with_flex_child(widget.center(), 1.0)
        .with_child(
            Painter::new(|ctx, _: &_, _: &_| {
                let size = ctx.size().to_rect();
                ctx.fill(size, &Color::WHITE)
            })
            .fix_height(1.0),
        )
        .with_child(Label::new(label).center().fix_height(40.0))
        .border(Color::WHITE, 1.0)
}

fn load_xi_image<Ctx: druid::RenderContext>(ctx: &mut Ctx) -> Ctx::Image {
    ctx.make_image(32, 32, XI_IMAGE, druid::piet::ImageFormat::Rgb)
        .unwrap()
}

fn paint_example<T>(ctx: &mut PaintCtx, _: &T, _env: &Env) {
    let bounds = ctx.size().to_rect();
    let img = load_xi_image(ctx.render_ctx);
    ctx.draw_image(
        &img,
        bounds,
        druid::piet::InterpolationMode::NearestNeighbor,
    );
    ctx.with_save(|ctx| {
        ctx.transform(Affine::scale_non_uniform(bounds.width(), bounds.height()));
        // Draw the dot of the `i` on top of the image data.
        let i_dot = Circle::new((0.775, 0.18), 0.05);
        let i_dot_brush = ctx.solid_brush(Color::WHITE);
        ctx.fill(i_dot, &i_dot_brush);
        // Cross out Xi because it's going dormant :'(
        let mut spare = BezPath::new();
        spare.move_to((0.1, 0.1));
        spare.line_to((0.2, 0.1));
        spare.line_to((0.9, 0.9));
        spare.line_to((0.8, 0.9));
        spare.close_path();
        let spare_brush = ctx
            .gradient(FixedLinearGradient {
                start: (0.0, 0.0).into(),
                end: (1.0, 1.0).into(),
                stops: vec![
                    GradientStop {
                        pos: 0.0,
                        color: Color::rgb(1.0, 0.0, 0.0),
                    },
                    GradientStop {
                        pos: 1.0,
                        color: Color::rgb(0.4, 0.0, 0.0),
                    },
                ],
            })
            .unwrap();
        ctx.fill(spare, &spare_brush);
    });
}

// Grid widget

const DEFAULT_GRID_CELL_SIZE: Size = Size::new(100.0, 100.0);
const DEFAULT_GRID_SPACING: f64 = 10.0;

pub struct SquaresGrid<T> {
    widgets: Vec<WidgetPod<T, Box<dyn Widget<T>>>>,
    /// The number of widgets we can fit in the grid given the grid size.
    drawable_widgets: usize,
    cell_size: Size,
    spacing: f64,
}

impl<T> SquaresGrid<T> {
    pub fn new() -> Self {
        SquaresGrid {
            widgets: vec![],
            drawable_widgets: 0,
            cell_size: DEFAULT_GRID_CELL_SIZE,
            spacing: DEFAULT_GRID_SPACING,
        }
    }

    pub fn with_spacing(mut self, spacing: f64) -> Self {
        self.spacing = spacing;
        self
    }

    pub fn with_cell_size(mut self, cell_size: Size) -> Self {
        self.cell_size = cell_size;
        self
    }

    pub fn with_child(mut self, widget: impl Widget<T> + 'static) -> Self {
        self.widgets.push(WidgetPod::new(Box::new(widget)));
        self
    }
}

impl<T> Default for SquaresGrid<T> {
    fn default() -> Self {
        Self::new()
    }
}

impl<T: Data> Widget<T> for SquaresGrid<T> {
    fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env: &Env) {
        for widget in self.widgets.iter_mut() {
            widget.event(ctx, event, data, env);
        }
    }

    fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &T, env: &Env) {
        for widget in self.widgets.iter_mut() {
            widget.lifecycle(ctx, event, data, env);
        }
    }

    fn update(&mut self, ctx: &mut UpdateCtx, _old_data: &T, data: &T, env: &Env) {
        for widget in self.widgets.iter_mut() {
            widget.update(ctx, data, env);
        }
    }

    fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &T, env: &Env) -> Size {
        let count = self.widgets.len() as f64;
        // The space needed to lay all elements out on a single line.
        let ideal_width = (self.cell_size.width + self.spacing + 1.0) * count;
        // Constrain the width.
        let width = ideal_width.min(bc.max().width).max(bc.min().width);
        // Given the width, the space needed to lay out all elements (as many as possible on each
        // line).
        let cells_in_row =
            ((width - self.spacing) / (self.cell_size.width + self.spacing)).floor() as usize;
        let (height, rows) = if cells_in_row > 0 {
            let mut rows = (count / cells_in_row as f64).ceil() as usize;
            let height_from_rows =
                |rows: usize| (rows as f64) * (self.cell_size.height + self.spacing) + self.spacing;
            let ideal_height = height_from_rows(rows);

            // Constrain the height
            let height = ideal_height.max(bc.min().height).min(bc.max().height);
            // Now calculate how many rows we can actually fit in
            while height_from_rows(rows) > height && rows > 0 {
                rows -= 1;
            }
            (height, rows)
        } else {
            (bc.min().height, 0)
        };
        // Constrain the number of drawn widgets by the number there is space to draw.
        self.drawable_widgets = self.widgets.len().min(rows * cells_in_row);
        // Now we have the width and height, we can lay out the children.
        let mut x_position = self.spacing;
        let mut y_position = self.spacing;
        for (idx, widget) in self
            .widgets
            .iter_mut()
            .take(self.drawable_widgets)
            .enumerate()
        {
            widget.layout(
                ctx,
                &BoxConstraints::new(self.cell_size, self.cell_size),
                data,
                env,
            );
            widget.set_origin(ctx, Point::new(x_position, y_position));
            // Increment position for the next cell
            x_position += self.cell_size.width + self.spacing;
            // If we can't fit in another cell in this row ...
            if (idx + 1) % cells_in_row == 0 {
                // ... then start a new row.
                x_position = self.spacing;
                y_position += self.cell_size.height + self.spacing;
            }
        }
        Size { width, height }
    }

    fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) {
        for widget in self.widgets.iter_mut().take(self.drawable_widgets) {
            widget.paint(ctx, data, env);
        }
    }
}