// 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, 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 { #[cfg(feature = "svg")] let svg_example = label_widget( Svg::new( include_str!("./assets/tiger.svg") .parse::() .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(widget: impl Widget + 'static, label: &str) -> impl Widget { 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: &mut Ctx) -> Ctx::Image { ctx.make_image(32, 32, XI_IMAGE, druid::piet::ImageFormat::Rgb) .unwrap() } fn paint_example(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 { widgets: Vec>>>, /// The number of widgets we can fit in the grid given the grid size. drawable_widgets: usize, cell_size: Size, spacing: f64, } impl SquaresGrid { 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 + 'static) -> Self { self.widgets.push(WidgetPod::new(Box::new(widget))); self } } impl Default for SquaresGrid { fn default() -> Self { Self::new() } } impl Widget for SquaresGrid { 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); } } }