Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Calculator: make_widget and grid layout

Topics: grid layout, access keys

Calculator

extern crate kas;
use std::num::ParseFloatError;
use std::str::FromStr;

use kas::event::NamedKey;
use kas::prelude::*;
use kas::widgets::{AccessLabel, Adapt, Button, EditBox, column, grid};

type Key = kas::event::Key<kas::event::SmolStr>;

fn key_button(label: &str) -> Button<AccessLabel> {
    let string = AccessString::from(label);
    let key = string.key().unwrap().clone();
    Button::label_msg(string, key)
}
fn key_button_with(label: &str, key: Key) -> Button<AccessLabel> {
    Button::label_msg(label, key.clone()).with_access_key(key)
}

fn calc_ui() -> Window<()> {
    // We could use kas::widget::Text, but EditBox looks better.
    let display = EditBox::string(|calc: &Calculator| calc.display())
        .with_multi_line(true)
        .with_lines(3.0, 3.0)
        .with_width_em(5.0, 10.0);

    let buttons = grid! {
        // Key bindings: C, Del
        (0, 0) => Button::label_msg("&clear", Key::Named(NamedKey::Clear))
            .with_access_key(NamedKey::Delete.into()),
        // Widget is hidden but has key binding.
        // TODO(opt): exclude from layout & drawing.
        (0, 0) => key_button_with("", NamedKey::Backspace.into()),
        (1, 0) => key_button_with("&÷", Key::Character("/".into())),
        (2, 0) => key_button_with("&×", Key::Character("*".into())),
        (3, 0) => key_button_with("&−", Key::Character("-".into())),
        (0, 1) => key_button("&7"),
        (1, 1) => key_button("&8"),
        (2, 1) => key_button("&9"),
        (3, 1..3) => key_button("&+"),
        (0, 2) => key_button("&4"),
        (1, 2) => key_button("&5"),
        (2, 2) => key_button("&6"),
        (0, 3) => key_button("&1"),
        (1, 3) => key_button("&2"),
        (2, 3) => key_button("&3"),
        (3, 3..5) => key_button_with("&=", NamedKey::Enter.into()),
        (0..2, 4) => key_button("&0"),
        (2, 4) => key_button("&."),
    }
    .map_any();

    let ui = Adapt::new(column![display, buttons], Calculator::new())
        .on_message(|_, calc, key| calc.handle(key));

    Window::new(ui, "Calculator")
        .escapable()
        .with_alt_bypass()
        .without_nav_focus()
}

fn main() -> kas::runner::Result<()> {
    env_logger::init();

    let theme = kas_wgpu::ShadedTheme::new();
    let mut app = kas::runner::Runner::with_theme(theme).build(())?;
    let _ = app.config_mut().font.set_size(24.0);
    app.with(calc_ui()).run()
}

#[derive(Clone, Debug)]
struct Calculator {
    // ...
}

impl Calculator {
    fn new() -> Calculator {
        Calculator {
            // ...
        }
    }

    fn display(&self) -> String {
        // ...
        String::new()
    }

    fn handle(&mut self, key: Key) {
        // ...
        let _ = key;
    }
}

The back-end: Calculator

First things first, lets define our backend, Calculator. It must have:

  • Internal state (fields)
  • A constructor (new)
  • Some type of output. A display function returning a String will do.
  • A handler for button presses. We'll just pass a Key to fn handle.

Fill out the implementation yourself or copy from the full source.

Access keys

To make the calculator keyboard-accessible, we'll use access keys (see more on Wikipedia or Windows app docs). Kas supports these via:

Button helper fns

To make constructing buttons easier, we define some helper functions. (These facilitate defining the button message more than they do the access keys.)

#![allow(unused)]
fn main() {
extern crate kas;
use kas::text::AccessString;
use kas::widgets::{AccessLabel, Button};
type Key = kas::event::Key<kas::event::SmolStr>;
fn key_button(label: &str) -> Button<AccessLabel> {
    let string = AccessString::from(label);
    let key = string.key().unwrap().clone();
    Button::label_msg(string, key)
}
fn key_button_with(label: &str, key: Key) -> Button<AccessLabel> {
    Button::label_msg(label, key.clone()).with_access_key(key)
}
}

Normally, access keys are only active while holding Alt. To avoid this requirement we call with_alt_bypass. Further, we disable Tab key navigation with without_nav_focus and ensure that the window can be closed with the Esc key.

#![allow(unused)]
fn main() {
extern crate kas;
use kas::{Widget, widgets::{Label, Adapt}, window::Window};
#[derive(Debug)]
struct Calculator;
impl Calculator {
  fn new() -> Self { Calculator }
  fn handle(&mut self, _key: ()) {}
}
fn ui() -> impl Widget<Data = ()> {
let ui = Label::new_any("");
    Window::new(ui, "Calculator")
        .escapable()
        .with_alt_bypass()
        .without_nav_focus()
}
}

Grid layout

We already saw column and row layouts. This time, we'll use grid! for layout.

#![allow(unused)]
fn main() {
extern crate kas;
use kas::event::NamedKey;
use kas::prelude::*;
use kas::widgets::{AccessLabel, Button, grid};
type Key = kas::event::Key<kas::event::SmolStr>;
fn key_button(label: &str) -> Button<AccessLabel> {
    let string = AccessString::from(label);
    let key = string.key().unwrap().clone();
    Button::label_msg(string, key)
}
fn key_button_with(label: &str, key: Key) -> Button<AccessLabel> {
    Button::label_msg(label, key.clone()).with_access_key(key)
}
fn ui() -> impl Widget<Data = i32> {
    let buttons = grid! {
        // Key bindings: C, Del
        (0, 0) => Button::label_msg("&clear", Key::Named(NamedKey::Clear))
            .with_access_key(NamedKey::Delete.into()),
        // Widget is hidden but has key binding.
        // TODO(opt): exclude from layout & drawing.
        (0, 0) => key_button_with("", NamedKey::Backspace.into()),
        (1, 0) => key_button_with("&÷", Key::Character("/".into())),
        (2, 0) => key_button_with("&×", Key::Character("*".into())),
        (3, 0) => key_button_with("&−", Key::Character("-".into())),
        (0, 1) => key_button("&7"),
        (1, 1) => key_button("&8"),
        (2, 1) => key_button("&9"),
        (3, 1..3) => key_button("&+"),
        (0, 2) => key_button("&4"),
        (1, 2) => key_button("&5"),
        (2, 2) => key_button("&6"),
        (0, 3) => key_button("&1"),
        (1, 3) => key_button("&2"),
        (2, 3) => key_button("&3"),
        (3, 3..5) => key_button_with("&=", NamedKey::Enter.into()),
        (0..2, 4) => key_button("&0"),
        (2, 4) => key_button("&."),
    }
    .map_any();
buttons
}
}

Worth noting is our hidden Backspace button. This is just another cell, but hidden under the clear button. Yes, this is a sub-optimal hack.

Again, we use .map_any() to make our buttons (input Data = ()) compatible with the parent UI element (input Data = Calculator).