use geom::{Distance, Polygon, Pt2D};
use crate::{
svg, text::Font, Color, ContentMode, ControlState, CornerRounding, Drawable, EdgeInsets,
EventCtx, GeomBatch, GfxCtx, Line, MultiKey, Outcome, RewriteColor, ScreenDims, ScreenPt,
ScreenRectangle, Text, Widget, WidgetImpl, WidgetOutput,
};
pub struct Button {
pub action: String,
draw_normal: Drawable,
draw_hovered: Drawable,
draw_disabled: Drawable,
pub(crate) hotkey: Option<MultiKey>,
tooltip: Text,
hitbox: Polygon,
pub(crate) hovering: bool,
is_disabled: bool,
pub(crate) top_left: ScreenPt,
pub(crate) dims: ScreenDims,
}
impl Button {
fn new(
ctx: &EventCtx,
normal: GeomBatch,
hovered: GeomBatch,
disabled: GeomBatch,
hotkey: Option<MultiKey>,
action: &str,
maybe_tooltip: Option<Text>,
hitbox: Polygon,
is_disabled: bool,
) -> Button {
let bounds = hitbox.get_bounds();
let dims = ScreenDims::new(bounds.width(), bounds.height());
assert!(!action.is_empty());
Button {
action: action.to_string(),
draw_normal: ctx.upload(normal),
draw_hovered: ctx.upload(hovered),
draw_disabled: ctx.upload(disabled),
tooltip: if let Some(t) = maybe_tooltip {
t
} else {
Text::tooltip(ctx, hotkey.clone(), action)
},
hotkey,
hitbox,
is_disabled,
hovering: false,
top_left: ScreenPt::new(0.0, 0.0),
dims,
}
}
pub fn is_enabled(&self) -> bool {
!self.is_disabled
}
}
impl WidgetImpl for Button {
fn get_dims(&self) -> ScreenDims {
self.dims
}
fn set_pos(&mut self, top_left: ScreenPt) {
self.top_left = top_left;
}
fn event(&mut self, ctx: &mut EventCtx, output: &mut WidgetOutput) {
if ctx.redo_mouseover() {
if let Some(pt) = ctx.canvas.get_cursor_in_screen_space() {
self.hovering = self
.hitbox
.translate(self.top_left.x, self.top_left.y)
.contains_pt(pt.to_pt());
} else {
self.hovering = false;
}
}
if self.is_disabled {
return;
}
if self.hovering && ctx.normal_left_click() {
self.hovering = false;
output.outcome = Outcome::Clicked(self.action.clone());
return;
}
if ctx.input.pressed(self.hotkey.clone()) {
self.hovering = false;
output.outcome = Outcome::Clicked(self.action.clone());
return;
}
if self.hovering {
ctx.cursor_clickable();
}
}
fn draw(&self, g: &mut GfxCtx) {
if self.is_disabled {
g.redraw_at(self.top_left, &self.draw_disabled);
} else if self.hovering {
g.redraw_at(self.top_left, &self.draw_hovered);
if !self.tooltip.is_empty() {
g.draw_mouse_tooltip(self.tooltip.clone());
}
} else {
g.redraw_at(self.top_left, &self.draw_normal);
}
}
}
#[derive(Clone, Debug, Default)]
pub struct ButtonBuilder<'a> {
padding: EdgeInsets,
stack_spacing: f64,
hotkey: Option<MultiKey>,
tooltip: Option<Text>,
stack_axis: Option<geom_batch_stack::Axis>,
is_label_before_image: bool,
corner_rounding: Option<CornerRounding>,
is_disabled: bool,
default_style: ButtonStyle<'a>,
hover_style: ButtonStyle<'a>,
disable_style: ButtonStyle<'a>,
}
#[derive(Clone, Debug, Default)]
struct ButtonStyle<'a> {
image: Option<Image<'a>>,
label: Option<Label<'a>>,
outline: Option<(f64, Color)>,
bg_color: Option<Color>,
custom_batch: Option<GeomBatch>,
}
impl<'b, 'a: 'b> ButtonBuilder<'a> {
pub fn new() -> Self {
ButtonBuilder {
padding: EdgeInsets {
top: 8.0,
bottom: 8.0,
left: 16.0,
right: 16.0,
},
stack_spacing: 10.0,
..Default::default()
}
}
pub fn padding<EI: Into<EdgeInsets>>(mut self, padding: EI) -> Self {
self.padding = padding.into();
self
}
pub fn padding_top(mut self, padding: f32) -> Self {
self.padding.top = padding;
self
}
pub fn padding_left(mut self, padding: f32) -> Self {
self.padding.left = padding;
self
}
pub fn padding_bottom(mut self, padding: f32) -> Self {
self.padding.bottom = padding;
self
}
pub fn padding_right(mut self, padding: f32) -> Self {
self.padding.right = padding;
self
}
pub fn label_text(mut self, text: &'a str) -> Self {
let mut label = self.default_style.label.take().unwrap_or_default();
label.text = Some(text);
self.default_style.label = Some(label);
self
}
pub fn label_styled_text(mut self, styled_text: Text, for_state: ControlState) -> Self {
let state_style = self.style_mut(for_state);
let mut label = state_style.label.take().unwrap_or_default();
label.styled_text = Some(styled_text);
label.text = None;
state_style.label = Some(label);
self
}
pub fn label_color(mut self, color: Color, for_state: ControlState) -> Self {
let state_style = self.style_mut(for_state);
let mut label = state_style.label.take().unwrap_or_default();
label.color = Some(color);
state_style.label = Some(label);
self
}
pub fn font(mut self, font: Font) -> Self {
let mut label = self.default_style.label.take().unwrap_or_default();
label.font = Some(font);
self.default_style.label = Some(label);
self
}
pub fn font_size(mut self, font_size: usize) -> Self {
let mut label = self.default_style.label.take().unwrap_or_default();
label.font_size = Some(font_size);
self.default_style.label = Some(label);
self
}
pub fn image_path(mut self, path: &'a str) -> Self {
let mut image = self.default_style.image.take().unwrap_or_default();
image.source = Some(ImageSource::Path(path));
self.default_style.image = Some(image);
self
}
pub fn image_bytes(mut self, labeled_bytes: (&'a str, &'a [u8])) -> Self {
let (label, bytes) = labeled_bytes;
let mut image = self.default_style.image.take().unwrap_or_default();
image.source = Some(ImageSource::Bytes {
bytes,
cache_key: label,
});
self.default_style.image = Some(image);
self
}
pub fn image_color<C: Into<RewriteColor>>(mut self, color: C, for_state: ControlState) -> Self {
let state_style = self.style_mut(for_state);
let mut image = state_style.image.take().unwrap_or_default();
image.color = Some(color.into());
state_style.image = Some(image);
self
}
pub fn image_bg_color(mut self, color: Color, for_state: ControlState) -> Self {
let state_style = self.style_mut(for_state);
let mut image = state_style.image.take().unwrap_or_default();
image.bg_color = Some(color);
state_style.image = Some(image);
self
}
pub fn image_dims<D: Into<ScreenDims>>(mut self, dims: D) -> Self {
let mut image = self.default_style.image.take().unwrap_or_default();
image.dims = Some(dims.into());
self.default_style.image = Some(image);
self
}
pub fn image_content_mode(mut self, content_mode: ContentMode) -> Self {
let mut image = self.default_style.image.take().unwrap_or_default();
image.content_mode = content_mode;
self.default_style.image = Some(image);
self
}
pub fn image_corner_rounding<R: Into<CornerRounding>>(mut self, value: R) -> Self {
let mut image = self.default_style.image.take().unwrap_or_default();
image.corner_rounding = Some(value.into());
self.default_style.image = Some(image);
self
}
pub fn image_padding<EI: Into<EdgeInsets>>(mut self, value: EI) -> Self {
let mut image = self.default_style.image.take().unwrap_or_default();
image.padding = Some(value.into());
self.default_style.image = Some(image);
self
}
pub fn bg_color(mut self, color: Color, for_state: ControlState) -> Self {
self.style_mut(for_state).bg_color = Some(color);
self
}
pub fn outline(mut self, thickness: f64, color: Color, for_state: ControlState) -> Self {
self.style_mut(for_state).outline = Some((thickness, color));
self
}
pub fn custom_batch(mut self, batch: GeomBatch, for_state: ControlState) -> Self {
self.style_mut(for_state).custom_batch = Some(batch);
self
}
pub fn hotkey<MK: Into<MultiKey>>(mut self, key: MK) -> Self {
self.hotkey = Some(key.into());
self
}
pub fn tooltip(mut self, tooltip: Text) -> Self {
self.tooltip = Some(tooltip);
self
}
pub fn no_tooltip(mut self) -> Self {
self.tooltip = Some(Text::new());
self
}
pub fn vertical(mut self) -> Self {
self.stack_axis = Some(geom_batch_stack::Axis::Vertical);
self
}
pub fn horizontal(mut self) -> Self {
self.stack_axis = Some(geom_batch_stack::Axis::Horizontal);
self
}
pub fn disabled(mut self, is_disabled: bool) -> Self {
self.is_disabled = is_disabled;
self
}
pub fn label_first(mut self) -> Self {
self.is_label_before_image = true;
self
}
pub fn image_first(mut self) -> Self {
self.is_label_before_image = false;
self
}
pub fn stack_spacing(mut self, value: f64) -> Self {
self.stack_spacing = value;
self
}
pub fn corner_rounding<R: Into<CornerRounding>>(mut self, value: R) -> Self {
self.corner_rounding = Some(value.into());
self
}
pub fn build(&self, ctx: &EventCtx, action: &str) -> Button {
let normal = self.batch(ctx, ControlState::Default);
let hovered = self.batch(ctx, ControlState::Hovered);
let disabled = self.batch(ctx, ControlState::Disabled);
assert!(
normal.get_bounds() != geom::Bounds::zero(),
"button was empty"
);
let hitbox = normal.get_bounds().get_rectangle();
Button::new(
ctx,
normal,
hovered,
disabled,
self.hotkey.clone(),
action,
self.tooltip.clone(),
hitbox,
self.is_disabled,
)
}
pub fn build_widget(&self, ctx: &EventCtx, action: &str) -> Widget {
Widget::new(Box::new(self.build(ctx, action))).named(action)
}
pub fn build_def(&self, ctx: &EventCtx) -> Widget {
let action = self
.default_style
.label
.as_ref()
.and_then(|label| label.text)
.expect("Must set `label_text` before calling build_def");
self.build_widget(ctx, action)
}
fn style_mut(&'b mut self, state: ControlState) -> &'b mut ButtonStyle<'a> {
match state {
ControlState::Default => &mut self.default_style,
ControlState::Hovered => &mut self.hover_style,
ControlState::Disabled => &mut self.disable_style,
}
}
fn style(&'b self, state: ControlState) -> &'b ButtonStyle<'a> {
match state {
ControlState::Default => &self.default_style,
ControlState::Hovered => &self.hover_style,
ControlState::Disabled => &self.disable_style,
}
}
fn batch(&self, ctx: &EventCtx, for_state: ControlState) -> GeomBatch {
let state_style = self.style(for_state);
if let Some(custom_batch) = state_style.custom_batch.as_ref() {
return custom_batch.clone();
}
let default_style = &self.default_style;
if let Some(custom_batch) = default_style.custom_batch.as_ref() {
return custom_batch.clone();
}
let image_batch = state_style
.image
.as_ref()
.or(default_style.image.as_ref())
.and_then(|image| {
let default = default_style.image.as_ref();
let image_source = image
.source
.as_ref()
.or(default.and_then(|d| d.source.as_ref()));
if image_source.is_none() {
return None;
}
let image_source = image_source.unwrap();
let (mut svg_batch, svg_bounds) = image_source.load(ctx.prerender);
if let Some(color) = image.color.or(default.and_then(|d| d.color)) {
svg_batch = svg_batch.color(color);
}
if let Some(image_dims) = image.dims.or(default.and_then(|d| d.dims)) {
if svg_bounds.width() != 0.0 && svg_bounds.height() != 0.0 {
let (x_factor, y_factor) = (
image_dims.width / svg_bounds.width(),
image_dims.height / svg_bounds.height(),
);
svg_batch = match image.content_mode {
ContentMode::ScaleToFill => svg_batch.scale_xy(x_factor, y_factor),
ContentMode::ScaleAspectFit => svg_batch.scale(x_factor.min(y_factor)),
ContentMode::ScaleAspectFill => svg_batch.scale(x_factor.max(y_factor)),
}
}
let image_corners = image
.corner_rounding
.or(default.and_then(|d| d.corner_rounding))
.unwrap_or_default();
let padding = image
.padding
.or(default.and_then(|d| d.padding))
.unwrap_or_default();
let mut container_batch = GeomBatch::new();
let container = match image_corners {
CornerRounding::FullyRounded => Polygon::pill(
image_dims.width + padding.left as f64 + padding.right as f64,
image_dims.height + padding.top as f64 + padding.bottom as f64,
),
CornerRounding::CornerRadii(image_corners) => {
Polygon::rounded_rectangle(
image_dims.width + padding.left as f64 + padding.right as f64,
image_dims.height + padding.top as f64 + padding.bottom as f64,
image_corners,
)
}
};
let image_bg = image
.bg_color
.or(default.and_then(|d| d.bg_color))
.unwrap_or(Color::CLEAR);
container_batch.push(image_bg, container);
let center = Pt2D::new(
image_dims.width / 2.0 + padding.left as f64,
image_dims.height / 2.0 + padding.top as f64,
);
svg_batch = svg_batch.autocrop().centered_on(center);
container_batch.append(svg_batch);
svg_batch = container_batch
}
Some(svg_batch)
});
let label_batch = state_style
.label
.as_ref()
.or(default_style.label.as_ref())
.and_then(|label| {
let default = default_style.label.as_ref();
if let Some(styled_text) = label
.styled_text
.as_ref()
.or(default.and_then(|d| d.styled_text.as_ref()))
{
return Some(styled_text.clone().bg(Color::CLEAR).render(ctx));
}
let text = label.text.or(default.and_then(|d| d.text));
if text.is_none() {
return None;
}
let text = text.unwrap();
let color = label
.color
.or(default.and_then(|d| d.color))
.unwrap_or(ctx.style().outline_color);
let mut line = Line(text).fg(color);
if let Some(font_size) = label.font_size.or(default.and_then(|d| d.font_size)) {
line = line.size(font_size);
}
if let Some(font) = label.font.or(default.and_then(|d| d.font)) {
line = line.font(font);
}
Some(
Text::from(line)
.bg(Color::CLEAR)
.render(ctx),
)
});
use geom_batch_stack::Stack;
let mut stack = Stack::horizontal();
if let Some(stack_axis) = self.stack_axis {
stack.set_axis(stack_axis);
}
stack.spacing(self.stack_spacing);
let mut items = vec![];
if let Some(image_batch) = image_batch {
items.push(image_batch);
}
if let Some(label_batch) = label_batch {
items.push(label_batch);
}
if self.is_label_before_image {
items.reverse()
}
stack.append(&mut items);
let mut button_widget = stack
.batch()
.batch()
.container()
.padding(self.padding)
.bg(state_style
.bg_color
.or(default_style.bg_color)
.unwrap_or(Color::CLEAR));
if let Some((thickness, color)) = state_style.outline.or(default_style.outline) {
button_widget = button_widget.outline(thickness, color);
}
if let Some(corner_rounding) = self.corner_rounding {
button_widget = button_widget.corner_rounding(corner_rounding);
}
let (geom_batch, _hitbox) = button_widget.to_geom(ctx, None);
geom_batch
}
}
#[derive(Clone, Debug)]
enum ImageSource<'a> {
Path(&'a str),
Bytes { bytes: &'a [u8], cache_key: &'a str },
}
impl ImageSource<'_> {
fn load(&self, prerender: &crate::Prerender) -> (GeomBatch, geom::Bounds) {
match self {
ImageSource::Path(image_path) => svg::load_svg(prerender, image_path),
ImageSource::Bytes { bytes, cache_key } => {
svg::load_svg_bytes(prerender, cache_key, bytes).expect(&format!(
"Failed to load svg from bytes. cache_key: {}",
cache_key
))
}
}
}
}
#[derive(Clone, Debug, Default)]
pub struct Image<'a> {
source: Option<ImageSource<'a>>,
color: Option<RewriteColor>,
bg_color: Option<Color>,
dims: Option<ScreenDims>,
content_mode: ContentMode,
corner_rounding: Option<CornerRounding>,
padding: Option<EdgeInsets>,
}
#[derive(Clone, Debug, Default)]
struct Label<'a> {
text: Option<&'a str>,
color: Option<Color>,
styled_text: Option<Text>,
font_size: Option<usize>,
font: Option<Font>,
}
pub struct MultiButton {
draw: Drawable,
hitboxes: Vec<(Polygon, String)>,
hovering: Option<usize>,
top_left: ScreenPt,
dims: ScreenDims,
}
impl MultiButton {
pub fn new(ctx: &EventCtx, batch: GeomBatch, hitboxes: Vec<(Polygon, String)>) -> Widget {
Widget::new(Box::new(MultiButton {
dims: batch.get_dims(),
top_left: ScreenPt::new(0.0, 0.0),
draw: ctx.upload(batch),
hitboxes,
hovering: None,
}))
}
}
impl WidgetImpl for MultiButton {
fn get_dims(&self) -> ScreenDims {
self.dims
}
fn set_pos(&mut self, top_left: ScreenPt) {
self.top_left = top_left;
}
fn event(&mut self, ctx: &mut EventCtx, output: &mut WidgetOutput) {
if ctx.redo_mouseover() {
self.hovering = None;
if let Some(cursor) = ctx.canvas.get_cursor_in_screen_space() {
if !ScreenRectangle::top_left(self.top_left, self.dims).contains(cursor) {
return;
}
let translated =
ScreenPt::new(cursor.x - self.top_left.x, cursor.y - self.top_left.y).to_pt();
for (idx, (region, _)) in self.hitboxes.iter().enumerate() {
if region.contains_pt(translated) {
self.hovering = Some(idx);
break;
}
}
}
}
if let Some(idx) = self.hovering {
if ctx.normal_left_click() {
self.hovering = None;
output.outcome = Outcome::Clicked(self.hitboxes[idx].1.clone());
}
}
}
fn draw(&self, g: &mut GfxCtx) {
g.redraw_at(self.top_left, &self.draw);
if let Some(idx) = self.hovering {
if let Ok(p) = self.hitboxes[idx].0.to_outline(Distance::meters(1.0)) {
let draw = g.upload(GeomBatch::from(vec![(Color::YELLOW, p)]));
g.redraw_at(self.top_left, &draw);
}
}
}
}
mod geom_batch_stack {
use crate::GeomBatch;
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Axis {
Horizontal,
Vertical,
}
#[derive(Debug)]
pub(crate) struct Stack {
batches: Vec<GeomBatch>,
axis: Axis,
spacing: f64,
}
impl Default for Stack {
fn default() -> Self {
Stack {
batches: vec![],
axis: Axis::Horizontal,
spacing: 0.0,
}
}
}
impl Stack {
pub fn horizontal() -> Self {
Stack {
axis: Axis::Horizontal,
..Default::default()
}
}
#[allow(unused)]
pub fn vertical() -> Self {
Stack {
axis: Axis::Vertical,
..Default::default()
}
}
pub fn set_axis(&mut self, new_value: Axis) {
self.axis = new_value;
}
#[allow(unused)]
pub fn push(&mut self, geom_batch: GeomBatch) {
self.batches.push(geom_batch);
}
pub fn append(&mut self, geom_batches: &mut Vec<GeomBatch>) {
self.batches.append(geom_batches);
}
pub fn spacing(&mut self, spacing: f64) -> &mut Self {
self.spacing = spacing;
self
}
pub fn batch(self) -> GeomBatch {
if self.batches.is_empty() {
return GeomBatch::new();
}
let max_bound_for_axis = self
.batches
.iter()
.map(GeomBatch::get_bounds)
.max_by(|b1, b2| match self.axis {
Axis::Vertical => b1.width().partial_cmp(&b2.width()).unwrap(),
Axis::Horizontal => b1.height().partial_cmp(&b2.height()).unwrap(),
})
.unwrap();
let mut stack_batch = GeomBatch::new();
let mut stack_offset = 0.0;
for mut batch in self.batches {
let bounds = batch.get_bounds();
let alignment_inset = match self.axis {
Axis::Vertical => (max_bound_for_axis.width() - bounds.width()) / 2.0,
Axis::Horizontal => (max_bound_for_axis.height() - bounds.height()) / 2.0,
};
let (dx, dy) = match self.axis {
Axis::Vertical => (alignment_inset, stack_offset),
Axis::Horizontal => (stack_offset, alignment_inset),
};
batch = batch.translate(dx, dy);
stack_batch.append(batch);
stack_offset += self.spacing;
match self.axis {
Axis::Vertical => stack_offset += bounds.height(),
Axis::Horizontal => stack_offset += bounds.width(),
}
}
stack_batch
}
}
}