Making the popup show faster

webneat

Amine Ben hammou

Posted on June 6, 2022

Making the popup show faster

Hello 👋,

In the previous article, I shared how I started working on a small productivity tool called focus. The idea of the tool is simple:

  • I hit a keyboard shortcut anywhere on my computer.
  • A popup shows on the center of the screen containing a text input.
  • I type some idea/task I want to remember later, then I hit Enter.
  • The popup disappears.
  • All ideas I type are added to a text file. The path of that file is set by the environment variable FOCUS_TASKS_PATH.

Last time, I had a working version, but there is one problem: When I hit the keyboard shortcut, the popup takes about 2 seconds to open and render correctly. Let's make it "pop up" faster.

Contents

Why is it slow? it's just a text input, right?

From my understanding of Tauri's process model, when I hit the shortcut, the following happens:

  • A core process is created. This is the process that runs the Rust code.
  • A webview process is created by the core process. This is the once running the frontend code.

You can think of a webview as a mini-browser that has everything needed to show a web page and handle interactions on it (parsing HTML, applying CSS, executing Javascript, ... ). Plus it can communicate with the core process to run Rust code.

This explains why it takes time the start it (like it takes time to open Chrome or VSCode).

How to make it open faster?

My idea is to open the app once and keep it running in the background so that when the shortcut is hit, we only show the window which should be faster than creating it from scratch. One way to do that is to add the app to the system tray (the section that has the small icons on the taskbar. You know the one that you close an app but still find its icon there telling you "I am still alive!").

Adding the application to the system tray

By following the documentation, first, we need to specify the icon of our app on the system tray using the tauri.conf.json file, let's use the already existing Tauri icon (I may need to design a custom icon for this application, but not a priority for now).

{
  "tauri": {
    // ...
    "systemTray": {
      "iconPath": "icons/32x32.png"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Next, I modified the dependencies section of the Cargo.toml file to include the feature gtk-tray which is needed on my Linux distribution.

tauri = { version = "1.0.0-rc.11", features = ["api-all", "gtk-tray", "system-tray"] }
Enter fullscreen mode Exit fullscreen mode

Now we need to edit the main.rs file to create the system tray and specify the menu items attached to it

use tauri::*;

fn make_tray() -> SystemTray {     // <- a function that creates the system tray
  let menu = SystemTrayMenu::new()
    .add_item(CustomMenuItem::new("toggle".to_string(), "Hide"))
    .add_item(CustomMenuItem::new("quit".to_string(), "Quit"));
  return SystemTray::new().with_menu(menu);
}

fn main() {
  tauri::Builder::default()
    .system_tray(make_tray())  // <- adding the system tray to the application
    .invoke_handler(tauri::generate_handler![add_task])
    .run(tauri::generate_context!())
    .expect("error while running tauri application");
}
Enter fullscreen mode Exit fullscreen mode

The make_tray function creates the system tray with a menu containing two items: Hide and Quit.

  • Initially, the application window is shown; I want to be able to hide it by clicking the Hide menu item. Then its text should become Show and clicking it should show the window again.
  • Clicking on the Quit menu item should close the application.

If I run the application now, I see the Tauri icon on the system tray. Clicking on it shows the menu with Hide and Quit items, but clicking them does nothing. To run some code when the items are clicked we should add an event handler using the on_system_tray_event method:

fn main() {
  tauri::Builder::default()
    .system_tray(make_tray())
    .on_system_tray_event(handle_tray_event)  // <- handling the system tray events
    .invoke_handler(tauri::generate_handler![add_task])
    .run(tauri::generate_context!())
    .expect("error while running tauri application");
}
Enter fullscreen mode Exit fullscreen mode

Then we define the handle_tray_event function:

fn handle_tray_event(app: &AppHandle, event: SystemTrayEvent) {
  if let SystemTrayEvent::MenuItemClick { id, .. } = event {
    if id.as_str() == "quit" {
      process::exit(0);
    }
    if id.as_str() == "toggle" {
      let window = app.get_window("main").unwrap();
      let menu_item = app.tray_handle().get_item("toggle");
      if window.is_visible().unwrap() {
        window.hide();
        menu_item.set_title("Show");
      } else {
        window.show();
        window.center();
        menu_item.set_title("Hide");
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The result:

System tray menu

Hiding the window from the frontend

The next step is to hide the window when Enter is typed on the input instead of closing the application. To do so, let's update the App component's code:

//...
import { appWindow } from '@tauri-apps/api/window'

function App() {
  const [content, setContent] = React.useState('')
  const handleKeyDown = async (e: React.KeyboardEvent) => {
    if (e.key === 'Enter') {
      await invoke('add_task', { content })
      await appWindow.hide()
    }
  }
  // ...
}
Enter fullscreen mode Exit fullscreen mode

Now when I type something and hit Enter, the window is hidden but there are two problems:

  1. The menu item still shows Hide when I click the icon on the system tray.
  2. When the window is shown again, the text I typed last time is still there.

Let's start by fixing the second problem which is easier. All we have to do is to set the content state to an empty string after invoking the add_task command.

function App() {
  const [content, setContent] = React.useState('')
  const handleKeyDown = async (e: React.KeyboardEvent) => {
    if (e.key === 'Enter') {
      await invoke('add_task', { content })
      setContent('')
      await appWindow.hide()
    }
  }
  // ...
}
Enter fullscreen mode Exit fullscreen mode

The first problem is trickier. I didn't find a way to change the menu item text using the Javascript API. So it seems that I need to call Rust code to do that. Let's create a new command hide_window:

#[tauri::command]
fn hide_window(app: AppHandle) {
  let window = app.get_window("main").unwrap();
  let menu_item = app.tray_handle().get_item("toggle");
  window.hide();
  menu_item.set_title("Show");
}
//...
fn main() {
  tauri::Builder::default()
    .system_tray(make_tray())
    .on_system_tray_event(handle_tray_event)
    .invoke_handler(tauri::generate_handler![add_task, hide_window]) // <- added the command here
    .run(tauri::generate_context!())
    .expect("error while running tauri application");
}
Enter fullscreen mode Exit fullscreen mode

And call it from the frontend:

function App() {
  const [content, setContent] = React.useState('')
  const handleKeyDown = async (e: React.KeyboardEvent) => {
    if (e.key === 'Enter') {
      await invoke('add_task', { content })
      setContent('')
      await invoke('hide_window')
    }
  }
  //...
}
Enter fullscreen mode Exit fullscreen mode

Note that the hide_window command takes an argument app of type AppHandle, but we don't pass any argument when calling it from Javascript. Tauri will inject that argument automatically based on its type.

Show the window with a global shortcut

So far, I was running a new instance of the application when typing the shortcut, now I want to just show the hidden window and not open a whole new instance. Tauri offers a way to register global shortcuts and run custom code when they are pressed.

Let's enable global shortcuts on tauri.conf.json

{
  "tauri": {
    "allowlist": {
      "globalShortcut": {
        "all": true
      }
    }
  },
  // ...
}
Enter fullscreen mode Exit fullscreen mode

Then register the shortcut Alt+Enter by adding the following code to the main.tsx file:

import { register } from '@tauri-apps/api/globalShortcut'

register('Alt+Enter', () => {
  console.log('Alt+Enter pressed!')
})
Enter fullscreen mode Exit fullscreen mode

Now the string Alt+Enter pressed! should be logged into the console every time I press Alt+Enter. But it doesn't work and I didn't know why ... until I found this open issue on the tao repository: https://github.com/tauri-apps/tao/issues/307

Tao is the Rust library Tauri uses to create and manage windows. And according to that issue, global shortcuts don't work properly on Linux, that's why it's not working for me!

So if I want to continue working on this application, I need to implement a workaround for shortcuts to work on Ubuntu. That's what I will be doing in the next blog post. See ya!

💖 💪 🙅 🚩
webneat
Amine Ben hammou

Posted on June 6, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related