emilk / egui

egui: an easy-to-use immediate mode GUI in Rust that runs on both web and native
https://www.egui.rs/
Apache License 2.0
22.43k stars 1.6k forks source link

Add mnemonic support #429

Open Vanadiae opened 3 years ago

Vanadiae commented 3 years ago

(Probably part of https://github.com/emilk/egui/issues/167)

Egui is currently nice enough to use with Tab/Shift+Tab, but it would be even nicer if there was support for mnemonics (also called accelerators sometimes) because it allows activating any UI element (e.g. button, text entry) without tabbing through all the UI to reach it. In the GTK toolkit (and maybe others? I only know GTK so I can't tell but it's likely the same for e.g. Qt), it is indicated by adding an underscore before the letter that should be used as mnemonic, so for example _Save or P_references. I think a proper place to have the mnemonic indicated would be the WidgetInfo's label field. I suppose that if someone really wants to use a raw underscore in the label then an exception with a double underscore could be added.

A proper implementation of mnemonics should make sure of the following things:

Ideally it should be possible to have a standalone label that would handle the mnemonic for an other widget (like a label next to a single line entry), but I don't think that would be easily compatible with the immediate-mode nature of egui?

Please let me know if anything is unclear :)

jgarvin commented 3 years ago

Currently for web navigation I use the Vimium plugin for Chrome. When I press F, all links get dynamically highlighted with a letter, and then pressing that letter will click that link. Importantly this doesn't require any deliberate accessibility effort from web developers to work. I was thinking of opening an issue for making that functionality work with egui, which I believe would involve it tricking the browser into thinking there are links wherever there are clickable widgets. I don't know what the conventions are like on native platforms for this... I've just gotten used to all apps having poor accessibility and just making voice commands that map to whatever keyboard shortcuts the app has.

So a couple questions:

akhilman commented 3 years ago

Is it better for egui to just generate a corresponding key to hit on the fly in response to the user pressing a common "I desire to click something" button, like Vimium?

The developer-defined mnemonic is stable and does not require reading before being pressed. Try this in gimp: Alt+F, N, Alt+O, Alt+R, R, O, P, Alt+O, and you will get same result every time.

emilk commented 3 years ago

Mnemonics with underscore under letters would be a nice addition, but needs to be implemented separately for Button, Checkbox etc. I think I would prefer something explicit like Button::new("Save").shortcut(Key::S). The button/checkbox would then find the first matching letter to highlight if Alt is pressed, and click itself if Alt+S is pressed. It is not a lot of work to implement (now that there is a text layout engine where one can easily underline pieces of the text).

emilk commented 3 years ago
* Does the web version of this merit a separate issue?

If we want to support Vimium etc we need to create fake anchor elements in the web view that matches the egui hyperlinks/buttons. This is a very big task, and does require a separate issue to discuss.

jgarvin commented 3 years ago

I think I would prefer something explicit like Button::new("Save").shortcut(Key::S).

I don’t know if this is in scope for egui, but I would guess part of the motivation for the string approach would be internationalization. Usually frameworks for it help substitute all your string literals appropriately, and different countries have different keyboards. I’m speculating though, never had to deal with it.

akhilman commented 3 years ago

Yes. In the gtk mnemonics is also part of the localization.

I don’t know if this is in scope for egui, but I would guess part of the motivation for the string approach would be internationalization. Usually frameworks for it help substitute all your string literals appropriately, and different countries have different keyboards. I’m speculating though, never had to deal with it.

albx79 commented 2 years ago

Java swing works the same, with an underscore in the label string used to specify the accelerator. This is good for internationalization.

emilk commented 2 years ago

Worth noting here: keyboard shortcuts can only work for things that are actually visible on screen. If a menu isn't open, the content code is never run, so any shortcuts to a menu-item (say File -> Save) must be handled somewhere else than where the button actually is.

As for layout: if you want to have underscore mean "add an underline to the next character", then it is pretty easy to write a function that creates a button with such an underline:

/// `if mnemonic_button(ui, "_Save").clicked() { … }`
fn mnemonic_button(ui: &Ui, text: &str) -> Response {
    let underscore_pos = text.find('_').unwrap(); // TODO: error handling
    let before = &text[..underscore_pos];
    let underscore = &text[underscore_pos..underscore_pos+1];
    let after = &text[underscore_pos+1..];

    let font_id = TextStyle::Button.resolve(&ui);    
    let color = ui.visuals().text_color();
    let underline = Stroke::new(1.0, color);
    let text_format = TextFormat { font_id, color, ..Default::default() };

    let mut job = LayoutJob::default();
    job.append(before, 0.0, text_format);
    job.append(underscore, 0.0, TextFormat { underline, ..text_format });
    job.append(after, 0.0, text_format);

    ui.button(job)
}

perhaps good enough for a start?