widgetry/widgets/
toggle.rs

1use crate::svg::load_svg_bytes;
2use crate::{
3    include_labeled_bytes, Button, Color, ControlState, EdgeInsets, EventCtx, GfxCtx, MultiKey,
4    Outcome, RewriteColor, ScreenDims, ScreenPt, Text, TextSpan, Widget, WidgetImpl, WidgetOutput,
5};
6
7pub struct Toggle {
8    pub(crate) enabled: bool,
9    pub(crate) btn: Button,
10    other_btn: Button,
11}
12
13impl Toggle {
14    pub fn new_widget(enabled: bool, false_btn: Button, true_btn: Button) -> Widget {
15        if enabled {
16            Widget::new(Box::new(Toggle {
17                enabled,
18                btn: true_btn,
19                other_btn: false_btn,
20            }))
21        } else {
22            Widget::new(Box::new(Toggle {
23                enabled,
24                btn: false_btn,
25                other_btn: true_btn,
26            }))
27        }
28    }
29
30    pub fn switch<MK: Into<Option<MultiKey>>>(
31        ctx: &EventCtx,
32        label: &str,
33        hotkey: MK,
34        enabled: bool,
35    ) -> Widget {
36        let mut buttons = ctx
37            .style()
38            .btn_plain
39            .text(label)
40            // we don't want the default coloring, because we do custom coloring below
41            .image_color(RewriteColor::NoOp, ControlState::Default);
42
43        if let Some(hotkey) = hotkey.into() {
44            buttons = buttons.hotkey(hotkey);
45        }
46
47        let (off_batch, off_bounds) = {
48            let (label, bytes) = include_labeled_bytes!("../../icons/switch_off.svg");
49            let (batch, bounds) = load_svg_bytes(ctx.prerender, label, bytes).expect("invalid SVG");
50            let batch = batch
51                .color(RewriteColor::Change(Color::WHITE, ctx.style.btn_solid.bg))
52                .color(RewriteColor::Change(Color::BLACK, ctx.style.btn_solid.fg));
53            (batch, bounds)
54        };
55        let (on_batch, on_bounds) = {
56            let (label, bytes) = include_labeled_bytes!("../../icons/switch_on.svg");
57            let (batch, bounds) = load_svg_bytes(ctx.prerender, label, bytes).expect("invalid SVG");
58            let batch = batch
59                .color(RewriteColor::Change(Color::WHITE, ctx.style.btn_solid.bg))
60                .color(RewriteColor::Change(Color::BLACK, ctx.style.btn_solid.fg));
61            (batch, bounds)
62        };
63
64        let off_button = buttons
65            .clone()
66            .image_batch(off_batch, off_bounds)
67            .build(ctx, label);
68
69        let on_button = buttons.image_batch(on_batch, on_bounds).build(ctx, label);
70
71        Toggle::new_widget(enabled, off_button, on_button).named(label)
72    }
73
74    pub fn checkbox<MK: Into<Option<MultiKey>>>(
75        ctx: &EventCtx,
76        label: &str,
77        hotkey: MK,
78        enabled: bool,
79    ) -> Widget {
80        let mut false_btn = ctx
81            .style()
82            .btn_plain
83            .icon_bytes(include_labeled_bytes!("../../icons/checkbox_unchecked.svg"))
84            .image_color(
85                RewriteColor::Change(Color::BLACK, ctx.style().icon_fg),
86                ControlState::Default,
87            )
88            .image_color(
89                RewriteColor::Change(Color::BLACK, ctx.style().icon_fg),
90                ControlState::Hovered,
91            )
92            .image_color(
93                RewriteColor::Change(Color::BLACK, ctx.style().icon_fg),
94                ControlState::Disabled,
95            )
96            .label_text(label);
97
98        if let Some(hotkey) = hotkey.into() {
99            false_btn = false_btn.hotkey(hotkey);
100        }
101
102        let true_btn = false_btn
103            .clone()
104            .image_bytes(include_labeled_bytes!("../../icons/checkbox_checked.svg"));
105
106        Toggle::new_widget(
107            enabled,
108            false_btn.build(ctx, label),
109            true_btn.build(ctx, label),
110        )
111        .named(label)
112    }
113
114    pub fn custom_checkbox<MK: Into<Option<MultiKey>>>(
115        ctx: &EventCtx,
116        action: &str,
117        spans: Vec<TextSpan>,
118        hotkey: MK,
119        enabled: bool,
120    ) -> Widget {
121        let mut false_btn = ctx
122            .style()
123            .btn_plain
124            .icon_bytes(include_labeled_bytes!("../../icons/checkbox_unchecked.svg"))
125            .image_color(
126                RewriteColor::Change(Color::BLACK, ctx.style().icon_fg),
127                ControlState::Default,
128            )
129            .image_color(
130                RewriteColor::Change(Color::BLACK, ctx.style().icon_fg),
131                ControlState::Hovered,
132            )
133            .image_color(
134                RewriteColor::Change(Color::BLACK, ctx.style().icon_fg),
135                ControlState::Disabled,
136            )
137            .label_styled_text(Text::from_all(spans), ControlState::Default);
138
139        if let Some(hotkey) = hotkey.into() {
140            false_btn = false_btn.hotkey(hotkey);
141        }
142
143        let true_btn = false_btn
144            .clone()
145            .image_bytes(include_labeled_bytes!("../../icons/checkbox_checked.svg"));
146
147        Toggle::new_widget(
148            enabled,
149            false_btn.build(ctx, action),
150            true_btn.build(ctx, action),
151        )
152        .named(action)
153    }
154
155    pub fn colored_checkbox(ctx: &EventCtx, label: &str, color: Color, enabled: bool) -> Widget {
156        let buttons = ctx.style().btn_plain.btn().label_text(label).padding(4.0);
157
158        let false_btn = buttons
159            .clone()
160            .image_bytes(include_labeled_bytes!(
161                "../../icons/checkbox_no_border_unchecked.svg"
162            ))
163            .image_color(
164                RewriteColor::Change(Color::BLACK, color.alpha(0.3)),
165                ControlState::Default,
166            );
167
168        let true_btn = buttons
169            .image_bytes(include_labeled_bytes!(
170                "../../icons/checkbox_no_border_checked.svg"
171            ))
172            .image_color(
173                RewriteColor::Change(Color::BLACK, color),
174                ControlState::Default,
175            );
176
177        Toggle::new_widget(
178            enabled,
179            false_btn.build(ctx, label),
180            true_btn.build(ctx, label),
181        )
182        .named(label)
183    }
184
185    // TODO These should actually be radio buttons
186    pub fn choice<MK: Into<Option<MultiKey>>>(
187        ctx: &EventCtx,
188        action: &str,
189        left_label: &str,
190        right_label: &str,
191        hotkey: MK,
192        enabled: bool,
193    ) -> Widget {
194        let mut toggle_left_button = ctx
195            .style()
196            .btn_plain
197            .btn()
198            .image_dims(ScreenDims::new(40.0, 40.0))
199            .padding(4)
200            // we don't want the default coloring, because we do custom coloring below
201            .image_color(RewriteColor::NoOp, ControlState::Default);
202
203        if let Some(hotkey) = hotkey.into() {
204            toggle_left_button = toggle_left_button.hotkey(hotkey);
205        }
206
207        let (left_batch, left_bounds) = {
208            let (label, bytes) = include_labeled_bytes!("../../icons/toggle_left.svg");
209            let (batch, bounds) = load_svg_bytes(ctx.prerender, label, bytes).expect("invalid SVG");
210            let batch = batch
211                .color(RewriteColor::Change(Color::WHITE, ctx.style.btn_solid.bg))
212                .color(RewriteColor::Change(Color::BLACK, ctx.style.btn_solid.fg));
213            (batch, bounds)
214        };
215        let (right_batch, right_bounds) = {
216            let (label, bytes) = include_labeled_bytes!("../../icons/toggle_right.svg");
217            let (batch, bounds) = load_svg_bytes(ctx.prerender, label, bytes).expect("invalid SVG");
218            let batch = batch
219                .color(RewriteColor::Change(Color::WHITE, ctx.style.btn_solid.bg))
220                .color(RewriteColor::Change(Color::BLACK, ctx.style.btn_solid.fg));
221            (batch, bounds)
222        };
223
224        let toggle_right_button = toggle_left_button
225            .clone()
226            .image_batch(right_batch, right_bounds);
227
228        let toggle_left_button = toggle_left_button
229            .clone()
230            .image_batch(left_batch, left_bounds);
231
232        let left_text_button = ctx
233            .style()
234            .btn_plain
235            .text(left_label)
236            // Cheat vertical padding to align with switch
237            .padding(EdgeInsets {
238                left: 2.0,
239                right: 2.0,
240                top: 8.0,
241                bottom: 14.0,
242            })
243            // TODO: make these clickable. Currently they would explode due to re-use of an action
244            .disabled(true)
245            .label_color(ctx.style().btn_outline.fg, ControlState::Disabled)
246            .bg_color(Color::CLEAR, ControlState::Disabled);
247        let right_text_button = left_text_button.clone().label_text(right_label);
248        Widget::row(vec![
249            left_text_button.build_def(ctx).centered_vert(),
250            Toggle::new_widget(
251                enabled,
252                toggle_right_button.build(ctx, right_label),
253                toggle_left_button.build(ctx, left_label),
254            )
255            .named(action)
256            .centered_vert(),
257            right_text_button.build_def(ctx).centered_vert(),
258        ])
259    }
260}
261
262impl WidgetImpl for Toggle {
263    fn get_dims(&self) -> ScreenDims {
264        self.btn.get_dims()
265    }
266
267    fn set_pos(&mut self, top_left: ScreenPt) {
268        self.btn.set_pos(top_left);
269    }
270
271    fn event(&mut self, ctx: &mut EventCtx, output: &mut WidgetOutput) {
272        self.btn.event(ctx, output);
273        if let Outcome::Clicked(_) = output.outcome {
274            // Both buttons have the same label
275            output.outcome = Outcome::Changed(self.btn.action.clone());
276            std::mem::swap(&mut self.btn, &mut self.other_btn);
277            self.btn.set_pos(self.other_btn.top_left);
278            self.enabled = !self.enabled;
279            output.redo_layout = true;
280        }
281    }
282
283    fn draw(&self, g: &mut GfxCtx) {
284        self.btn.draw(g);
285    }
286}