-
Notifications
You must be signed in to change notification settings - Fork 763
feat: added a splashscreen lab #2106
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
114937d
feat: added a splashscreen lab
simonhyll d31a147
feat: added show solution component
simonhyll c90e6f7
fix: formatting and cta usage
simonhyll a06b19f
fix: fragment import
simonhyll 79e4203
feat: more content for the lab
simonhyll 74fab31
fix: minor edit
simonhyll 07676e3
Update src/content/docs/learn/splashscreen.mdx
simonhyll 879c6c5
fix: splashscreen working now
simonhyll a20e0c4
Merge branch 'v2' into learn/splashscreen
simonhyll a642db2
fix: unwrap results
simonhyll File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
--- | ||
let { text } = Astro.props; | ||
text = text ?? "Show solution"; | ||
--- | ||
|
||
<show-solution class="show-solution"> | ||
<button>{text}</button> | ||
<div class="hidden"> | ||
<slot /> | ||
</div> | ||
</show-solution> | ||
|
||
<script> | ||
class ShowSolution extends HTMLElement { | ||
#contentDiv: HTMLDivElement | null = null; | ||
#button: HTMLButtonElement | null = null; | ||
constructor() { | ||
super(); | ||
this.#button = this.querySelector("button"); | ||
this.#button?.addEventListener("click", this.#toggle); | ||
this.#contentDiv = this.querySelector("div"); | ||
} | ||
#toggle = () => { | ||
if (this.#button) | ||
if (this.#contentDiv?.classList.contains("hidden")) { | ||
this.#contentDiv.classList.remove("hidden"); | ||
this.#button.innerText = this.#button.innerText.replace( | ||
"Show", | ||
"Hide" | ||
); | ||
} else { | ||
this.#contentDiv?.classList.add("hidden"); | ||
this.#button.innerText = this.#button.innerText.replace( | ||
"Hide", | ||
"Show" | ||
); | ||
} | ||
}; | ||
} | ||
customElements.define("show-solution", ShowSolution); | ||
</script> | ||
|
||
<style> | ||
.show-solution { | ||
display: block; | ||
width: 100%; | ||
margin-top: 1rem; | ||
} | ||
.show-solution button { | ||
width: 100%; | ||
background-color: var(--sl-color-gray-6); | ||
border: 1px solid var(--sl-color-gray-5); | ||
border-radius: 0.5rem; | ||
cursor: pointer; | ||
} | ||
.show-solution button:hover { | ||
background-color: var(--sl-color-gray-5); | ||
border: 1px solid var(--sl-color-gray-4); | ||
} | ||
.show-solution button:active { | ||
background-color: var(--sl-color-gray-4); | ||
border: 1px solid var(--sl-color-gray-3); | ||
} | ||
</style> | ||
|
||
<style is:global> | ||
.hidden { | ||
display: none !important; | ||
} | ||
</style> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,269 @@ | ||
--- | ||
title: Splashscreen | ||
sidebar: | ||
badge: | ||
text: WIP | ||
variant: caution | ||
tableOfContents: | ||
minHeadingLevel: 2 | ||
maxHeadingLevel: 5 | ||
--- | ||
|
||
import Stub from '@components/Stub.astro'; | ||
import { Image } from 'astro:assets'; | ||
import step_1 from '@assets/learn/splashscreen/step_1.png'; | ||
import step_3 from '@assets/learn/splashscreen/step_3.png'; | ||
import { Steps, Tabs, TabItem } from '@astrojs/starlight/components'; | ||
import ShowSolution from '@components/ShowSolution.astro'; | ||
import CTA from '@fragments/cta.mdx'; | ||
|
||
<Stub /> | ||
In this lab we'll be implementing a basic splashscreen functionality in a Tauri app. Doing so | ||
is quite straight forward, a splashscreen is effectively just a matter of creating a new window | ||
that displays some contents during the period your app is doing some heavy setup related tasks | ||
and then closing it when setting up is done. | ||
|
||
## Prerequisites | ||
|
||
:::tip[Create a lab app] | ||
|
||
If you are not an advanced user it's **highly recommended** that you use the options and frameworks provided here. It's just a lab, you can delete the project when you're done. | ||
|
||
<CTA/> | ||
|
||
- Project name: `splashscreen-lab` | ||
- Choose which language to use for your frontend: `Typescript / Javascript` | ||
- Choose your package manager: `pnpm` | ||
- Choose your UI template: `Vanilla` | ||
- Choose your UI flavor: `Typescript` | ||
- Would you like to setup the project for mobile as well? `yes` | ||
::: | ||
|
||
## Steps | ||
|
||
<Steps> | ||
|
||
1. ##### Install dependencies and run the project | ||
|
||
Before you start developing any project it's important to build and run the initial template, just to validate your setup is working as intended. | ||
|
||
<ShowSolution> | ||
```sh frame=none | ||
# Make sure you're in the right directory | ||
cd splashscreen-lab | ||
# Install dependencies | ||
pnpm install | ||
# Build and run the app | ||
pnpm tauri dev | ||
``` | ||
<Image src={step_1} alt="Successful run of the created template app."/> | ||
</ShowSolution> | ||
|
||
1. ##### Register new windows in `tauri.conf.json` | ||
|
||
The easiest way of adding new windows is by adding them directly to `tauri.conf.json`. You can also create them dynamically at startup, | ||
but for the sake of simplicity lets just register them instead. Make sure you have a window with the label `main` that's being created as a hidden window and a window with the label `splashscreen` that's created as being shown directly. You can leave all other options as their defaults, or tweak them based on preference. | ||
|
||
<ShowSolution> | ||
```json | ||
// src-tauri/tauri.conf.json | ||
{ | ||
"windows": [ | ||
{ | ||
"label": "main", | ||
"visible": false | ||
}, | ||
{ | ||
"label": "splashscreen", | ||
"url": "/splashscreen" | ||
} | ||
] | ||
} | ||
``` | ||
</ShowSolution> | ||
|
||
1. ##### Create a new page to host your splashscreen | ||
|
||
Before you begin you'll need to have some content to show. How you develop new pages depend on your chosen framework, | ||
most have the concept of a "router" that handles page navigation which should work just like normal in Tauri, in which case | ||
you just create a new splashscreen page. Or as we're going to be doing here, create a new `splashscreen.html` file to host the contents. | ||
|
||
What's important here is that you can navigate to a `/splashscreen` URL and be shown the contents you want for your splashscreen. Try running the app again after this step! | ||
|
||
<ShowSolution> | ||
```html | ||
// /splashscreen.html | ||
<!doctype html> | ||
<html lang="en"> | ||
<head> | ||
<meta charset="UTF-8" /> | ||
<link rel="stylesheet" href="/src/styles.css" /> | ||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
<title>Tauri App</title> | ||
</head> | ||
<body> | ||
<div class="container"> | ||
<h1>Tauri used Splash!</h1> | ||
<div class="row"> | ||
<h5>It was super effective!</h5> | ||
</div> | ||
</div> | ||
</body> | ||
</html> | ||
``` | ||
<Image src={step_3} alt="The splashscreen we just created."/> | ||
</ShowSolution> | ||
|
||
1. ##### Start some setup tasks | ||
|
||
Since splashscreens are generally intended to be used for the sake of hiding heavy setup related tasks, lets fake giving the app something heavy to do, some in the frontend and some in the backend. | ||
|
||
To fake heavy setup in the frontend we're going to be using a simple `setTimeout` function. | ||
|
||
The easiest way to fake heavy operations in the backend is by using the Tokio crate, which is the Rust crate that Tauri uses in the backend to provide an asynchronous runtime. While Tauri provides the runtime there are various utilities that Tauri doesn't re-export from it, so we'll need to add the crate to our project in order to access them. This is a perfectly normal practice within the Rust ecosystem. | ||
|
||
Don't use `std::thread::sleep` in async functions, they run cooperatively in a concurrent environment not in parallel, meaning that if you sleep the thread instead of the Tokio task you'll be locking all tasks scheduled to run on that thread from being executed, causing your app to freeze. | ||
|
||
<ShowSolution> | ||
```sh frame=none | ||
# Run this command where the `Cargo.toml` file is | ||
cd src-tauri | ||
# Add the Tokio crate | ||
cargo add tokio | ||
# Optionally go back to the top folder to keep developing | ||
# `tauri dev` can figure out where to run automatically | ||
cd .. | ||
``` | ||
|
||
```javascript | ||
// src/main.ts | ||
// These contents can be copy-pasted below the existing code, don't replace the entire file!! | ||
|
||
// Utility function to implement a sleep function in TypeScript | ||
function sleep(seconds: number): Promise<void> { | ||
return new Promise(resolve => setTimeout(resolve, seconds * 1000)); | ||
} | ||
|
||
// Setup function | ||
async function setup() { | ||
// Fake perform some really heavy setup task | ||
console.log('Performing really heavy frontend setup task...') | ||
await sleep(3); | ||
console.log('Frontend setup task complete!') | ||
// Set the frontend task as being completed | ||
invoke('set_complete', {task: 'frontend'}) | ||
} | ||
|
||
// Effectively a JavaScript main function | ||
window.addEventListener("DOMContentLoaded", () => { | ||
setup() | ||
}); | ||
``` | ||
|
||
```rust | ||
// /src-tauri/src/lib.rs | ||
// Import functionalities we'll be using | ||
use std::sync::Mutex; | ||
use tauri::async_runtime::spawn; | ||
use tauri::{AppHandle, Manager, State}; | ||
use tokio::time::{sleep, Duration}; | ||
|
||
// Create a struct we'll use to track the completion of | ||
// setup related tasks | ||
struct SetupState { | ||
frontend_task: bool, | ||
backend_task: bool, | ||
} | ||
|
||
// Our main entrypoint in a version 2 mobile compatible app | ||
#[cfg_attr(mobile, tauri::mobile_entry_point)] | ||
pub fn run() { | ||
// Don't write code before Tauri starts, write it in the | ||
// setup hook instead! | ||
tauri::Builder::default() | ||
// Register a `State` to be managed by Tauri | ||
// We need write access to it so we wrap it in a `Mutex` | ||
.manage(Mutex::new(SetupState { | ||
frontend_task: false, | ||
backend_task: false, | ||
})) | ||
// Add a command we can use to check | ||
.invoke_handler(tauri::generate_handler![greet, set_complete]) | ||
// Use the setup hook to execute setup related tasks | ||
// Runs before the main loop, so no windows are yet created | ||
.setup(|app| { | ||
// Spawn setup as a non-blocking task so the windows can be | ||
// created and ran while it executes | ||
spawn(setup(app.handle().clone())); | ||
// The hook expects an Ok result | ||
Ok(()) | ||
}) | ||
// Run the app | ||
.run(tauri::generate_context!()) | ||
.expect("error while running tauri application"); | ||
} | ||
|
||
#[tauri::command] | ||
fn greet(name: String) -> String { | ||
format!("Hello {name} from Rust!") | ||
} | ||
|
||
// A custom task for setting the state of a setup task | ||
#[tauri::command] | ||
async fn set_complete( | ||
app: AppHandle, | ||
state: State<'_, Mutex<SetupState>>, | ||
task: String, | ||
) -> Result<(), ()> { | ||
// Lock the state without write access | ||
let mut state_lock = state.lock().unwrap(); | ||
match task.as_str() { | ||
"frontend" => state_lock.frontend_task = true, | ||
"backend" => state_lock.backend_task = true, | ||
_ => panic!("invalid task completed!"), | ||
} | ||
// Check if both tasks are completed | ||
if state_lock.backend_task && state_lock.frontend_task { | ||
// Setup is complete, we can close the splashscreen | ||
// and unhide the main window! | ||
let splash_window = app.get_webview_window("splashscreen").unwrap(); | ||
let main_window = app.get_webview_window("main").unwrap(); | ||
splash_window.close().unwrap(); | ||
main_window.show().unwrap(); | ||
} | ||
Ok(()) | ||
} | ||
|
||
// An async function that does some heavy setup task | ||
async fn setup(app: AppHandle) -> Result<(), ()> { | ||
// Fake performing some heavy action for 3 seconds | ||
println!("Performing really heavy backend setup task..."); | ||
sleep(Duration::from_secs(3)).await; | ||
println!("Backend setup task completed!"); | ||
// Set the backend task as being completed | ||
// Commands can be ran as regular functions as long as you take | ||
// care of the input arguments yourself | ||
set_complete( | ||
app.clone(), | ||
app.state::<Mutex<SetupState>>(), | ||
"backend".to_string(), | ||
) | ||
.await?; | ||
Ok(()) | ||
} | ||
``` | ||
</ShowSolution> | ||
|
||
1. ##### Run the application | ||
|
||
You should now see a splashscreen window pop up, both the frontend and backend will perform their respective heavy 3 second setup tasks, after which the splashscreen disappears and the main window is shown! | ||
|
||
</Steps> | ||
|
||
## Discuss | ||
|
||
##### Should you have a splashscreen? | ||
|
||
In general having a splashscreen is an admittance of defeat that you couldn't make your | ||
app load fast enough to not need one. In fact it tends to be better to just go straight | ||
to a main window that then shows some little spinner somewhere in a corner informing the | ||
user there's still setup tasks happening in the background. | ||
|
||
However, with that said, it can be a stylistic choice that you want to have a splashscreen, | ||
or you might have some very particular requirement that makes it impossible to start the | ||
app until some tasks are performed. It's definitely not *wrong* to have a splashscreen, it | ||
just tends to not be necessary and can make users feel like the app isn't very well optimized. |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.