use geom::{Distance, Polygon};
use crate::{
svg, Color, Drawable, EdgeInsets, EventCtx, GeomBatch, GfxCtx, Key, Line, MultiKey, Outcome,
RewriteColor, ScreenDims, ScreenPt, ScreenRectangle, Text, Widget, WidgetImpl, WidgetOutput,
};
pub struct Button {
pub action: String,
draw_normal: Drawable,
draw_hovered: Drawable,
pub(crate) hotkey: Option<MultiKey>,
tooltip: Text,
hitbox: Polygon,
pub(crate) hovering: bool,
pub(crate) top_left: ScreenPt,
pub(crate) dims: ScreenDims,
}
impl Button {
fn new(
ctx: &EventCtx,
normal: GeomBatch,
hovered: GeomBatch,
hotkey: Option<MultiKey>,
tooltip: &str,
maybe_tooltip: Option<Text>,
hitbox: Polygon,
) -> Widget {
let bounds = hitbox.get_bounds();
let dims = ScreenDims::new(bounds.width(), bounds.height());
assert!(!tooltip.is_empty());
Widget::new(Box::new(Button {
action: tooltip.to_string(),
draw_normal: ctx.upload(normal),
draw_hovered: ctx.upload(hovered),
tooltip: if let Some(t) = maybe_tooltip {
t
} else {
Text::tooltip(ctx, hotkey.clone(), tooltip)
},
hotkey,
hitbox,
hovering: false,
top_left: ScreenPt::new(0.0, 0.0),
dims,
}))
.named(tooltip)
}
}
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.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.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);
}
}
}
pub struct Btn {}
impl Btn {
pub fn svg<I: Into<String>>(path: I, rewrite_hover: RewriteColor) -> BtnBuilder {
BtnBuilder::SVG {
path: path.into(),
rewrite_hover,
maybe_tooltip: None,
}
}
pub fn svg_def<I: Into<String>>(path: I) -> BtnBuilder {
BtnBuilder::SVG {
path: path.into(),
rewrite_hover: RewriteColor::ChangeAll(Color::ORANGE),
maybe_tooltip: None,
}
}
pub fn plaintext<I: Into<String>>(label: I) -> BtnBuilder {
let label = label.into();
BtnBuilder::PlainText {
label: label.clone(),
txt: Text::from(Line(label)),
maybe_tooltip: None,
}
}
pub fn plaintext_custom<I: Into<String>>(label: I, txt: Text) -> BtnBuilder {
BtnBuilder::PlainText {
label: label.into(),
txt,
maybe_tooltip: None,
}
}
pub fn text_fg<I: Into<String>>(label: I) -> BtnBuilder {
let label = label.into();
BtnBuilder::TextFG(label.clone(), Text::from(Line(label)), None)
}
pub fn txt<I: Into<String>>(label: I, txt: Text) -> BtnBuilder {
BtnBuilder::TextFG(label.into(), txt, None)
}
pub fn text_bg<I: Into<String>>(
label: I,
text: Text,
unselected_bg_color: Color,
selected_bg_color: Color,
) -> BtnBuilder {
BtnBuilder::TextBG {
label: label.into(),
maybe_tooltip: None,
text,
unselected_bg_color,
selected_bg_color,
}
}
pub fn text_bg1<I: Into<String>>(label: I) -> BtnBuilder {
let label = label.into();
BtnBuilder::TextBG {
label: label.clone(),
maybe_tooltip: None,
text: Text::from(Line(label)),
unselected_bg_color: Color::grey(0.5),
selected_bg_color: Color::ORANGE,
}
}
pub fn text_bg2<I: Into<String>>(label: I) -> BtnBuilder {
let label = label.into();
BtnBuilder::TextBG {
label: label.clone(),
maybe_tooltip: None,
text: Text::from(Line(label).fg(Color::hex("#5B5B5B"))),
unselected_bg_color: Color::WHITE,
selected_bg_color: Color::grey(0.8),
}
}
pub fn pop_up<I: Into<String>>(ctx: &EventCtx, label: Option<I>) -> BtnBuilder {
let mut icon_batch = GeomBatch::new();
let icon_container = Polygon::rectangle(20.0, 30.0);
icon_batch.push(Color::INVISIBLE, icon_container);
let icon = GeomBatch::from_svg_contents(
include_bytes!("../../../data/system/assets/widgetry/arrow_drop_down.svg").to_vec(),
)
.color(RewriteColor::ChangeAll(ctx.style().outline_color))
.autocrop()
.centered_on(icon_batch.get_bounds().center());
icon_batch.append(icon);
let button_geom = if let Some(label) = label {
let text = Text::from(Line(label));
let mut text_geom: GeomBatch = text.render(ctx);
text_geom.append(icon_batch.translate(text_geom.get_bounds().width() + 8.0, 0.0));
text_geom
} else {
icon_batch
};
let (button_geom, hitbox) = button_geom
.batch()
.container()
.padding(EdgeInsets {
top: 4.0,
bottom: 4.0,
left: 8.0,
right: 8.0,
})
.to_geom(ctx, None);
let hovered = button_geom.clone().color(RewriteColor::Change(
ctx.style().outline_color,
ctx.style().hovering_color,
));
let outline = (ctx.style().outline_thickness, ctx.style().outline_color);
BtnBuilder::Custom {
normal: button_geom,
hovered,
hitbox,
maybe_tooltip: None,
maybe_outline: Some(outline),
}
}
pub fn custom(
normal: GeomBatch,
hovered: GeomBatch,
hitbox: Polygon,
outline: Option<(f64, Color)>,
) -> BtnBuilder {
BtnBuilder::Custom {
normal,
hovered,
hitbox,
maybe_tooltip: None,
maybe_outline: outline,
}
}
pub fn close(ctx: &EventCtx) -> Widget {
Btn::plaintext("X")
.build(ctx, "close", Key::Escape)
.align_right()
}
}
pub enum BtnBuilder {
SVG {
path: String,
rewrite_hover: RewriteColor,
maybe_tooltip: Option<Text>,
},
TextFG(String, Text, Option<Text>),
PlainText {
label: String,
txt: Text,
maybe_tooltip: Option<Text>,
},
TextBG {
label: String,
maybe_tooltip: Option<Text>,
text: Text,
unselected_bg_color: Color,
selected_bg_color: Color,
},
Custom {
normal: GeomBatch,
hovered: GeomBatch,
hitbox: Polygon,
maybe_tooltip: Option<Text>,
maybe_outline: Option<(f64, Color)>,
},
}
impl BtnBuilder {
pub fn tooltip(mut self, tooltip: Text) -> BtnBuilder {
match self {
BtnBuilder::TextFG(_, _, ref mut maybe_tooltip)
| BtnBuilder::PlainText {
ref mut maybe_tooltip,
..
}
| BtnBuilder::Custom {
ref mut maybe_tooltip,
..
} => {
assert!(maybe_tooltip.is_none());
*maybe_tooltip = Some(tooltip);
}
BtnBuilder::SVG {
ref mut maybe_tooltip,
..
}
| BtnBuilder::TextBG {
ref mut maybe_tooltip,
..
} => {
assert!(maybe_tooltip.is_none());
*maybe_tooltip = Some(tooltip);
}
}
self
}
pub fn build<I: Into<String>, MK: Into<Option<MultiKey>>>(
self,
ctx: &EventCtx,
action_tooltip: I,
key: MK,
) -> Widget {
match self {
BtnBuilder::SVG {
path,
rewrite_hover,
maybe_tooltip,
} => {
let (normal, bounds) = svg::load_svg(ctx.prerender, &path);
let geom = Polygon::rectangle(bounds.width(), bounds.height());
let hovered = normal.clone().color(rewrite_hover);
Button::new(
ctx,
normal,
hovered,
key.into(),
&action_tooltip.into(),
maybe_tooltip,
geom,
)
}
BtnBuilder::TextFG(_, normal_txt, maybe_t) => {
let (normal, hitbox) = normal_txt
.clone()
.batch(ctx)
.container()
.padding(8)
.to_geom(ctx, None);
let (hovered, _) = normal_txt
.change_fg(Color::ORANGE)
.batch(ctx)
.container()
.padding(8)
.to_geom(ctx, None);
Button::new(
ctx,
normal,
hovered,
key.into(),
&action_tooltip.into(),
maybe_t,
hitbox,
)
.outline(2.0, Color::WHITE)
}
BtnBuilder::PlainText {
txt, maybe_tooltip, ..
} => {
let (normal, hitbox) = txt
.clone()
.batch(ctx)
.container()
.padding(8)
.to_geom(ctx, None);
let (hovered, _) = txt
.change_fg(Color::ORANGE)
.batch(ctx)
.container()
.padding(8)
.to_geom(ctx, None);
Button::new(
ctx,
normal,
hovered,
key.into(),
&action_tooltip.into(),
maybe_tooltip,
hitbox,
)
}
BtnBuilder::TextBG {
text,
maybe_tooltip,
unselected_bg_color,
selected_bg_color,
..
} => {
let (normal, hitbox) = text
.clone()
.batch(ctx)
.container()
.padding(15)
.bg(unselected_bg_color)
.to_geom(ctx, None);
let (hovered, _) = text
.batch(ctx)
.container()
.padding(15)
.bg(selected_bg_color)
.to_geom(ctx, None);
Button::new(
ctx,
normal,
hovered,
key.into(),
&action_tooltip.into(),
maybe_tooltip,
hitbox,
)
}
BtnBuilder::Custom {
normal,
hovered,
hitbox,
maybe_tooltip,
maybe_outline,
} => {
let button = Button::new(
ctx,
normal,
hovered,
key.into(),
&action_tooltip.into(),
maybe_tooltip,
hitbox,
);
if let Some(outline) = maybe_outline {
button.outline(outline.0, outline.1)
} else {
button
}
}
}
}
pub fn build_def<MK: Into<Option<MultiKey>>>(self, ctx: &EventCtx, key: MK) -> Widget {
match self {
BtnBuilder::SVG { .. } => panic!("Can't use build_def on an SVG button"),
BtnBuilder::Custom { .. } => panic!("Can't use build_def on a custom button"),
BtnBuilder::TextFG(ref label, _, _)
| BtnBuilder::PlainText { ref label, .. }
| BtnBuilder::TextBG { ref label, .. } => {
assert!(!label.is_empty());
let copy = label.clone();
self.build(ctx, copy, key)
}
}
}
pub fn inactive(self, ctx: &EventCtx) -> Widget {
match self {
BtnBuilder::TextFG(label, txt, _) => Widget::draw_batch(
ctx,
txt.change_fg(Color::grey(0.5))
.render(ctx)
.batch()
.container()
.padding(8)
.outline(2.0, Color::WHITE)
.to_geom(ctx, None)
.0,
)
.named(label),
BtnBuilder::TextBG {
text,
unselected_bg_color,
label,
..
} => {
assert_eq!(unselected_bg_color, Color::WHITE);
Widget::draw_batch(
ctx,
text.render(ctx)
.batch()
.container()
.padding(15)
.bg(Color::grey(0.7))
.to_geom(ctx, None)
.0,
)
.named(label)
}
BtnBuilder::PlainText { txt, label, .. } => Widget::draw_batch(
ctx,
txt.change_fg(Color::grey(0.5))
.render(ctx)
.batch()
.container()
.padding(8)
.to_geom(ctx, None)
.0,
)
.named(label),
_ => panic!("Can't use inactive on this kind of button"),
}
}
}
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);
}
}
}
}