Skip to content

Commit

Permalink
Add a widget gallery example.
Browse files Browse the repository at this point in the history
The `Grid` used to display them is also probably useful in other
situations.
  • Loading branch information
richard-uk1 committed Jun 11, 2020
1 parent bbbca65 commit db61de3
Show file tree
Hide file tree
Showing 3 changed files with 326 additions and 0 deletions.
1 change: 1 addition & 0 deletions druid/examples/web/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,4 @@ impl_example!(styled_text.unwrap());
impl_example!(switches);
impl_example!(timer);
impl_example!(view_switcher);
impl_example!(widget_gallery);
325 changes: 325 additions & 0 deletions druid/examples/widget_gallery.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,325 @@
// Copyright 2019 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.

//! Provides a grid to display the more complex widgets.
use std::sync::Arc;

use druid::{
widget::{
prelude::*, Button, Checkbox, Flex, Label, List, Painter, ProgressBar, RadioGroup, Scroll,
Slider, Stepper, Switch, TextBox,
},
AppLauncher, Color, Data, Lens, LocalizedString, Rect, Widget, WidgetExt, WidgetPod,
WindowDesc,
};

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

#[derive(Clone, Data, Lens)]
struct AppData {
label_data: String,
checkbox_data: bool,
clicked_count: u64,
list_items: Arc<Vec<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(LocalizedString::new("list-demo-window-title").with_placeholder("List Demo"));
// Set our initial data
let data = AppData {
label_data: "test".into(),
checkbox_data: false,
clicked_count: 0,
list_items: Arc::new(vec!["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)
.use_simple_logger()
.launch(data)
.expect("launch failed");
}

fn ui_builder() -> impl Widget<AppData> {
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(|ctx, _, _| {
let img = load_xi_image(ctx.render_ctx);
let rect = ctx.size().to_rect();
ctx.draw_image(&img, rect, druid::piet::InterpolationMode::NearestNeighbor);
})
.fix_size(32.0, 32.0),
"Painter",
))
.with_child(label_widget(
RadioGroup::new(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().lens(AppData::progressbar))
.with_spacer(4.0)
.with_child(Label::new(|data: &AppData, _: &_| {
format!("{:3.0}%", data.progressbar * 100.0)
})),
"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",
)),
)
.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()
}

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 calcuate 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()
{
let rect = Rect::new(
x_position,
y_position,
x_position + self.cell_size.width,
y_position + self.cell_size.height,
);
let size = widget.layout(
ctx,
&BoxConstraints::new(self.cell_size, self.cell_size),
data,
env,
);
assert_eq!(size, self.cell_size); // todo remove this
widget.set_layout_rect(ctx, data, env, rect);
// 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);
}
}
}
Binary file added druid/examples/xi.image
Binary file not shown.

0 comments on commit db61de3

Please sign in to comment.