feat: set application progress bar, close #7999 (#8009)

Co-authored-by: Lucas Nogueira <lucas@tauri.studio>
This commit is contained in:
Jason Tsai
2023-10-18 03:25:30 +08:00
committed by GitHub
parent a6ad540696
commit c085addab5
14 changed files with 426 additions and 206 deletions

View File

@@ -0,0 +1,8 @@
---
"tauri": 'patch:feat'
"tauri-runtime": 'patch:feat'
"tauri-runtime-wry": 'patch:feat'
"tauri-utils": 'patch:feat'
---
Added `set_progress_bar` to `Window`.

View File

@@ -0,0 +1,5 @@
---
"@tauri-apps/api": patch:feat
---
Added the `setProgressBar` API on the `Window` class.

View File

@@ -41,7 +41,9 @@ use wry::webview::WebViewBuilderExtWindows;
#[cfg(target_os = "macos")]
use tauri_utils::TitleBarStyle;
use tauri_utils::{config::WindowConfig, debug_eprintln, Theme};
use tauri_utils::{
config::WindowConfig, debug_eprintln, ProgressBarState, ProgressBarStatus, Theme,
};
use wry::{
application::{
dpi::{
@@ -56,8 +58,9 @@ use wry::{
},
monitor::MonitorHandle,
window::{
CursorIcon as WryCursorIcon, Fullscreen, Icon as WryWindowIcon, Theme as WryTheme,
UserAttentionType as WryUserAttentionType,
CursorIcon as WryCursorIcon, Fullscreen, Icon as WryWindowIcon,
ProgressBarState as WryProgressBarState, ProgressState as WryProgressState,
Theme as WryTheme, UserAttentionType as WryUserAttentionType,
},
},
webview::{FileDropEvent as WryFileDropEvent, Url, WebContext, WebView, WebViewBuilder},
@@ -520,6 +523,35 @@ impl From<CursorIcon> for CursorIconWrapper {
}
}
pub struct ProgressStateWrapper(pub WryProgressState);
impl From<ProgressBarStatus> for ProgressStateWrapper {
fn from(status: ProgressBarStatus) -> Self {
let state = match status {
ProgressBarStatus::None => WryProgressState::None,
ProgressBarStatus::Normal => WryProgressState::Normal,
ProgressBarStatus::Indeterminate => WryProgressState::Indeterminate,
ProgressBarStatus::Paused => WryProgressState::Paused,
ProgressBarStatus::Error => WryProgressState::Error,
};
Self(state)
}
}
pub struct ProgressBarStateWrapper(pub WryProgressBarState);
impl From<ProgressBarState> for ProgressBarStateWrapper {
fn from(progress_state: ProgressBarState) -> Self {
Self(WryProgressBarState {
progress: progress_state.progress,
state: progress_state
.status
.map(|state| ProgressStateWrapper::from(state).0),
unity_uri: progress_state.unity_uri,
})
}
}
#[derive(Clone, Default)]
pub struct WindowBuilderWrapper {
inner: WryWindowBuilder,
@@ -1006,6 +1038,7 @@ pub enum WindowMessage {
SetCursorIcon(CursorIcon),
SetCursorPosition(Position),
SetIgnoreCursorEvents(bool),
SetProgressBar(ProgressBarState),
DragWindow,
RequestRedraw,
}
@@ -1520,6 +1553,16 @@ impl<T: UserEvent> Dispatch<T> for WryDispatcher<T> {
),
)
}
fn set_progress_bar(&self, progress_state: ProgressBarState) -> Result<()> {
send_user_message(
&self.context,
Message::Window(
self.window_id,
WindowMessage::SetProgressBar(progress_state),
),
)
}
}
#[derive(Clone)]
@@ -2302,6 +2345,9 @@ fn handle_user_message<T: UserEvent>(
WindowMessage::RequestRedraw => {
window.request_redraw();
}
WindowMessage::SetProgressBar(progress_state) => {
window.set_progress_bar(ProgressBarStateWrapper::from(progress_state).0);
}
}
}
}

View File

@@ -15,7 +15,7 @@
use raw_window_handle::RawDisplayHandle;
use serde::Deserialize;
use std::{fmt::Debug, sync::mpsc::Sender};
use tauri_utils::Theme;
use tauri_utils::{ProgressBarState, Theme};
use url::Url;
/// Types useful for interacting with a user's monitors.
@@ -589,4 +589,12 @@ pub trait Dispatch<T: UserEvent>: Debug + Clone + Send + Sync + Sized + 'static
/// Executes javascript on the window this [`Dispatch`] represents.
fn eval_script<S: Into<String>>(&self, script: S) -> Result<()>;
/// Sets the taskbar progress state.
///
/// ## Platform-specific
///
/// - **Linux / macOS**: Progress bar is app-wide and not specific to this window. Only supported desktop environments with `libunity` (e.g. GNOME).
/// - **iOS / Android:** Unsupported.
fn set_progress_bar(&self, progress_state: ProgressBarState) -> Result<()>;
}

View File

@@ -416,3 +416,31 @@ pub fn display_path<P: AsRef<Path>>(p: P) -> String {
.display()
.to_string()
}
/// Progress bar status.
#[derive(Debug, Clone, Copy, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum ProgressBarStatus {
/// Hide progress bar.
None,
/// Normal state.
Normal,
/// Indeterminate state. **Treated as Normal on Linux and macOS**
Indeterminate,
/// Paused state. **Treated as Normal on Linux**
Paused,
/// Error state. **Treated as Normal on Linux**
Error,
}
/// Progress Bar State
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProgressBarState {
/// The progress bar status.
pub status: Option<ProgressBarStatus>,
/// The progress bar progress. This can be a value ranging from `0` to `100`
pub progress: Option<u64>,
/// The identifier for your app to communicate with the Unity desktop window manager **Linux Only**
pub unity_uri: Option<String>,
}

File diff suppressed because one or more lines are too long

View File

@@ -18,7 +18,7 @@ use tauri_runtime::{
#[cfg(target_os = "macos")]
use tauri_utils::TitleBarStyle;
use tauri_utils::{config::WindowConfig, Theme};
use tauri_utils::{config::WindowConfig, ProgressBarState, Theme};
use url::Url;
#[cfg(windows)]
@@ -676,6 +676,10 @@ impl<T: UserEvent> Dispatch<T> for MockDispatcher {
.replace(script.into());
Ok(())
}
fn set_progress_bar(&self, progress_state: ProgressBarState) -> Result<()> {
Ok(())
}
}
#[derive(Debug, Clone)]

View File

@@ -32,7 +32,10 @@ use crate::{
},
sealed::ManagerBase,
sealed::RuntimeOrDispatch,
utils::config::{WindowConfig, WindowEffectsConfig, WindowUrl},
utils::{
config::{WindowConfig, WindowEffectsConfig, WindowUrl},
ProgressBarState,
},
EventLoopMessage, Manager, Runtime, Theme, WindowEvent,
};
#[cfg(desktop)]
@@ -2044,6 +2047,21 @@ impl<R: Runtime> Window<R> {
pub fn start_dragging(&self) -> crate::Result<()> {
self.window.dispatcher.start_dragging().map_err(Into::into)
}
/// Sets the taskbar progress state.
///
/// ## Platform-specific
///
/// - **Linux / macOS**: Progress bar is app-wide and not specific to this window.
/// - **Linux**: Only supported desktop environments with `libunity` (e.g. GNOME).
/// - **iOS / Android:** Unsupported.
pub fn set_progress_bar(&self, progress_state: ProgressBarState) -> crate::Result<()> {
self
.window
.dispatcher
.set_progress_bar(progress_state)
.map_err(Into::into)
}
}
/// Webview APIs.

View File

@@ -12,6 +12,7 @@ use crate::{
#[cfg(desktop)]
mod desktop_commands {
use serde::Deserialize;
use tauri_utils::ProgressBarState;
use super::*;
use crate::{
@@ -153,6 +154,7 @@ mod desktop_commands {
setter!(set_cursor_position, Position);
setter!(set_ignore_cursor_events, bool);
setter!(start_dragging);
setter!(set_progress_bar, ProgressBarState);
setter!(print);
#[command(root = "crate")]
@@ -302,6 +304,7 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
desktop_commands::set_cursor_position,
desktop_commands::set_ignore_cursor_events,
desktop_commands::start_dragging,
desktop_commands::set_progress_bar,
desktop_commands::print,
desktop_commands::set_icon,
desktop_commands::toggle_maximize,

File diff suppressed because one or more lines are too long

View File

@@ -7,267 +7,281 @@
PhysicalPosition,
Effect,
EffectState,
ProgressBarStatus,
Window
} from "@tauri-apps/api/window";
import { invoke } from "@tauri-apps/api/primitives";
} from '@tauri-apps/api/window'
import { invoke } from '@tauri-apps/api/primitives'
const appWindow = getCurrent();
const appWindow = getCurrent()
let selectedWindow = appWindow.label;
let selectedWindow = appWindow.label
const windowMap = {
[appWindow.label]: appWindow,
};
[appWindow.label]: appWindow
}
const cursorIconOptions = [
"default",
"crosshair",
"hand",
"arrow",
"move",
"text",
"wait",
"help",
"progress",
'default',
'crosshair',
'hand',
'arrow',
'move',
'text',
'wait',
'help',
'progress',
// something cannot be done
"notAllowed",
"contextMenu",
"cell",
"verticalText",
"alias",
"copy",
"noDrop",
'notAllowed',
'contextMenu',
'cell',
'verticalText',
'alias',
'copy',
'noDrop',
// something can be grabbed
"grab",
'grab',
/// something is grabbed
"grabbing",
"allScroll",
"zoomIn",
"zoomOut",
'grabbing',
'allScroll',
'zoomIn',
'zoomOut',
// edge is to be moved
"eResize",
"nResize",
"neResize",
"nwResize",
"sResize",
"seResize",
"swResize",
"wResize",
"ewResize",
"nsResize",
"neswResize",
"nwseResize",
"colResize",
"rowResize",
];
'eResize',
'nResize',
'neResize',
'nwResize',
'sResize',
'seResize',
'swResize',
'wResize',
'ewResize',
'nsResize',
'neswResize',
'nwseResize',
'colResize',
'rowResize'
]
const windowsEffects = ["mica", "blur", "acrylic", "tabbed", "tabbedDark", "tabbedLight"];
const isWindows = navigator.appVersion.includes("Windows");
const isMacOS = navigator.appVersion.includes("Macintosh");
const windowsEffects = [
'mica',
'blur',
'acrylic',
'tabbed',
'tabbedDark',
'tabbedLight'
]
const isWindows = navigator.appVersion.includes('Windows')
const isMacOS = navigator.appVersion.includes('Macintosh')
let effectOptions = isWindows
? windowsEffects
: Object.keys(Effect)
.map((effect) => Effect[effect])
.filter((e) => !windowsEffects.includes(e));
.filter((e) => !windowsEffects.includes(e))
const effectStateOptions = Object.keys(EffectState).map(
(state) => EffectState[state]
);
)
export let onMessage;
const mainEl = document.querySelector("main");
const progressBarStatusOptions = Object.keys(ProgressBarStatus).map(s => ProgressBarStatus[s])
let newWindowLabel;
export let onMessage
const mainEl = document.querySelector('main')
let urlValue = "https://tauri.app";
let resizable = true;
let maximizable = true;
let minimizable = true;
let closable = true;
let maximized = false;
let decorations = true;
let alwaysOnTop = false;
let contentProtected = true;
let fullscreen = false;
let width = null;
let height = null;
let minWidth = null;
let minHeight = null;
let maxWidth = null;
let maxHeight = null;
let x = null;
let y = null;
let scaleFactor = 1;
let innerPosition = new PhysicalPosition(x, y);
let outerPosition = new PhysicalPosition(x, y);
let innerSize = new PhysicalSize(width, height);
let outerSize = new PhysicalSize(width, height);
let resizeEventUnlisten;
let moveEventUnlisten;
let cursorGrab = false;
let cursorVisible = true;
let cursorX = null;
let cursorY = null;
let cursorIcon = "default";
let cursorIgnoreEvents = false;
let windowTitle = "Awesome Tauri Example!";
let newWindowLabel
let effects = [];
let selectedEffect;
let effectState;
let effectRadius;
let effectR, effectG, effectB, effectA;
let urlValue = 'https://tauri.app'
let resizable = true
let maximizable = true
let minimizable = true
let closable = true
let maximized = false
let decorations = true
let alwaysOnTop = false
let contentProtected = true
let fullscreen = false
let width = null
let height = null
let minWidth = null
let minHeight = null
let maxWidth = null
let maxHeight = null
let x = null
let y = null
let scaleFactor = 1
let innerPosition = new PhysicalPosition(x, y)
let outerPosition = new PhysicalPosition(x, y)
let innerSize = new PhysicalSize(width, height)
let outerSize = new PhysicalSize(width, height)
let resizeEventUnlisten
let moveEventUnlisten
let cursorGrab = false
let cursorVisible = true
let cursorX = null
let cursorY = null
let cursorIcon = 'default'
let cursorIgnoreEvents = false
let windowTitle = 'Awesome Tauri Example!'
let windowIconPath;
let effects = []
let selectedEffect
let effectState
let effectRadius
let effectR, effectG, effectB, effectA
let selectedProgressBarStatus = 'none'
let progress = 0
let windowIconPath
function setTitle_() {
windowMap[selectedWindow].setTitle(windowTitle);
windowMap[selectedWindow].setTitle(windowTitle)
}
function hide_() {
windowMap[selectedWindow].hide();
setTimeout(windowMap[selectedWindow].show, 2000);
windowMap[selectedWindow].hide()
setTimeout(windowMap[selectedWindow].show, 2000)
}
function minimize_() {
windowMap[selectedWindow].minimize();
setTimeout(windowMap[selectedWindow].unminimize, 2000);
windowMap[selectedWindow].minimize()
setTimeout(windowMap[selectedWindow].unminimize, 2000)
}
function changeIcon() {
windowMap[selectedWindow].setIcon(path);
windowMap[selectedWindow].setIcon(path)
}
function createWindow() {
if (!newWindowLabel) return;
if (!newWindowLabel) return
const webview = new Window(newWindowLabel);
windowMap[newWindowLabel] = webview;
webview.once("tauri://error", function () {
onMessage("Error creating new webview");
});
const webview = new Window(newWindowLabel)
windowMap[newWindowLabel] = webview
webview.once('tauri://error', function () {
onMessage('Error creating new webview')
})
}
function loadWindowSize() {
windowMap[selectedWindow].innerSize().then((response) => {
innerSize = response;
width = innerSize.width;
height = innerSize.height;
});
innerSize = response
width = innerSize.width
height = innerSize.height
})
windowMap[selectedWindow].outerSize().then((response) => {
outerSize = response;
});
outerSize = response
})
}
function loadWindowPosition() {
windowMap[selectedWindow].innerPosition().then((response) => {
innerPosition = response;
});
innerPosition = response
})
windowMap[selectedWindow].outerPosition().then((response) => {
outerPosition = response;
x = outerPosition.x;
y = outerPosition.y;
});
outerPosition = response
x = outerPosition.x
y = outerPosition.y
})
}
async function addWindowEventListeners(window) {
if (!window) return;
if (!window) return
if (resizeEventUnlisten) {
resizeEventUnlisten();
resizeEventUnlisten()
}
if (moveEventUnlisten) {
moveEventUnlisten();
moveEventUnlisten()
}
moveEventUnlisten = await window.listen("tauri://move", loadWindowPosition);
resizeEventUnlisten = await window.listen("tauri://resize", loadWindowSize);
moveEventUnlisten = await window.listen('tauri://move', loadWindowPosition)
resizeEventUnlisten = await window.listen('tauri://resize', loadWindowSize)
}
async function requestUserAttention_() {
await windowMap[selectedWindow].minimize();
await windowMap[selectedWindow].minimize()
await windowMap[selectedWindow].requestUserAttention(
UserAttentionType.Critical
);
await new Promise((resolve) => setTimeout(resolve, 3000));
await windowMap[selectedWindow].requestUserAttention(null);
)
await new Promise((resolve) => setTimeout(resolve, 3000))
await windowMap[selectedWindow].requestUserAttention(null)
}
async function addEffect() {
if (!effects.includes(selectedEffect)) {
effects = [...effects, selectedEffect];
effects = [...effects, selectedEffect]
}
const payload = {
effects,
state: effectState,
radius: effectRadius,
};
radius: effectRadius
}
if (
Number.isInteger(effectR) &&
Number.isInteger(effectG) &&
Number.isInteger(effectB) &&
Number.isInteger(effectA)
) {
payload.color = [effectR, effectG, effectB, effectA];
payload.color = [effectR, effectG, effectB, effectA]
}
mainEl.classList.remove("bg-primary");
mainEl.classList.remove("dark:bg-darkPrimary");
await windowMap[selectedWindow].clearEffects();
await windowMap[selectedWindow].setEffects(payload);
mainEl.classList.remove('bg-primary')
mainEl.classList.remove('dark:bg-darkPrimary')
await windowMap[selectedWindow].clearEffects()
await windowMap[selectedWindow].setEffects(payload)
}
async function clearEffects() {
effects = [];
await windowMap[selectedWindow].clearEffects();
mainEl.classList.add("bg-primary");
mainEl.classList.add("dark:bg-darkPrimary");
effects = []
await windowMap[selectedWindow].clearEffects()
mainEl.classList.add('bg-primary')
mainEl.classList.add('dark:bg-darkPrimary')
}
$: {
windowMap[selectedWindow];
loadWindowPosition();
loadWindowSize();
windowMap[selectedWindow]
loadWindowPosition()
loadWindowSize()
}
$: windowMap[selectedWindow]?.setResizable(resizable);
$: windowMap[selectedWindow]?.setMaximizable(maximizable);
$: windowMap[selectedWindow]?.setMinimizable(minimizable);
$: windowMap[selectedWindow]?.setClosable(closable);
$: windowMap[selectedWindow]?.setResizable(resizable)
$: windowMap[selectedWindow]?.setMaximizable(maximizable)
$: windowMap[selectedWindow]?.setMinimizable(minimizable)
$: windowMap[selectedWindow]?.setClosable(closable)
$: maximized
? windowMap[selectedWindow]?.maximize()
: windowMap[selectedWindow]?.unmaximize();
$: windowMap[selectedWindow]?.setDecorations(decorations);
$: windowMap[selectedWindow]?.setAlwaysOnTop(alwaysOnTop);
$: windowMap[selectedWindow]?.setContentProtected(contentProtected);
$: windowMap[selectedWindow]?.setFullscreen(fullscreen);
: windowMap[selectedWindow]?.unmaximize()
$: windowMap[selectedWindow]?.setDecorations(decorations)
$: windowMap[selectedWindow]?.setAlwaysOnTop(alwaysOnTop)
$: windowMap[selectedWindow]?.setContentProtected(contentProtected)
$: windowMap[selectedWindow]?.setFullscreen(fullscreen)
$: width &&
height &&
windowMap[selectedWindow]?.setSize(new PhysicalSize(width, height));
windowMap[selectedWindow]?.setSize(new PhysicalSize(width, height))
$: minWidth && minHeight
? windowMap[selectedWindow]?.setMinSize(
new LogicalSize(minWidth, minHeight)
)
: windowMap[selectedWindow]?.setMinSize(null);
: windowMap[selectedWindow]?.setMinSize(null)
$: maxWidth > 800 && maxHeight > 400
? windowMap[selectedWindow]?.setMaxSize(
new LogicalSize(maxWidth, maxHeight)
)
: windowMap[selectedWindow]?.setMaxSize(null);
: windowMap[selectedWindow]?.setMaxSize(null)
$: x !== null &&
y !== null &&
windowMap[selectedWindow]?.setPosition(new PhysicalPosition(x, y));
windowMap[selectedWindow]?.setPosition(new PhysicalPosition(x, y))
$: windowMap[selectedWindow]
?.scaleFactor()
.then((factor) => (scaleFactor = factor));
$: addWindowEventListeners(windowMap[selectedWindow]);
.then((factor) => (scaleFactor = factor))
$: addWindowEventListeners(windowMap[selectedWindow])
$: windowMap[selectedWindow]?.setCursorGrab(cursorGrab);
$: windowMap[selectedWindow]?.setCursorVisible(cursorVisible);
$: windowMap[selectedWindow]?.setCursorIcon(cursorIcon);
$: windowMap[selectedWindow]?.setCursorGrab(cursorGrab)
$: windowMap[selectedWindow]?.setCursorVisible(cursorVisible)
$: windowMap[selectedWindow]?.setCursorIcon(cursorIcon)
$: cursorX !== null &&
cursorY !== null &&
windowMap[selectedWindow]?.setCursorPosition(
new PhysicalPosition(cursorX, cursorY)
);
$: windowMap[selectedWindow]?.setIgnoreCursorEvents(cursorIgnoreEvents);
)
$: windowMap[selectedWindow]?.setIgnoreCursorEvents(cursorIgnoreEvents)
$: windowMap[selectedWindow]?.setProgressBar({ status: selectedProgressBarStatus, progress })
</script>
<div class="flex flex-col children:grow gap-2">
@@ -293,9 +307,7 @@
{#if windowMap[selectedWindow]}
<br />
<div class="flex gap-1 items-center">
<label>
Icon path
</label>
<label> Icon path </label>
<form class="flex gap-1 grow" on:submit|preventDefault={setTitle_}>
<input class="input grow" bind:value={windowIconPath} />
<button class="btn" type="submit"> Change window icon </button>
@@ -527,6 +539,24 @@
<br />
<div class="flex flex-col gap-1">
<div class="flex gap-2">
<label>
Progress Status
<select class="input" bind:value={selectedProgressBarStatus}>
{#each progressBarStatusOptions as status}
<option value={status}>{status}</option>
{/each}
</select>
</label>
<label>
Progress
<input class="input" type="number" min="0" max="100" bind:value={progress} />
</label>
</div>
</div>
{#if isWindows || isMacOS}
<div class="flex flex-col gap-1">
<div class="flex">
@@ -598,7 +628,7 @@
<div class="flex">
<div>
Applied effects: {effects.length ? effects.join(",") : "None"}
Applied effects: {effects.length ? effects.join(',') : 'None'}
</div>
<button class="btn" style="width: 80px;" on:click={clearEffects}

View File

@@ -9,7 +9,11 @@ import { internalIpV4 } from 'internal-ip'
// https://vitejs.dev/config/
export default defineConfig(async ({ command, mode }) => {
const host = process.env.TAURI_PLATFORM === 'android' || process.env.TAURI_PLATFORM === 'ios' ? (await internalIpV4()) : 'localhost'
const host =
process.env.TAURI_PLATFORM === 'android' ||
process.env.TAURI_PLATFORM === 'ios'
? await internalIpV4()
: 'localhost'
return {
plugins: [Unocss(), svelte()],
build: {

File diff suppressed because one or more lines are too long

View File

@@ -148,6 +148,44 @@ export type CursorIcon =
| 'colResize'
| 'rowResize'
export enum ProgressBarStatus {
/**
* Hide progress bar.
*/
None = 'none',
/**
* Normal state.
*/
Normal = 'normal',
/**
* Indeterminate state. **Treated as Normal on Linux and macOS**
*/
Indeterminate = 'indeterminate',
/**
* Paused state. **Treated as Normal on Linux**
*/
Paused = 'paused',
/**
* Error state. **Treated as Normal on linux**
*/
Error = 'error'
}
export interface ProgressBarState {
/**
* The progress bar status.
*/
status?: ProgressBarStatus
/**
* The progress bar progress. This can be a value ranging from `0` to `100`
*/
progress?: number
/**
* The identifier for your app to communicate with the Unity desktop window manager **Linux Only**
*/
unityUri?: string
}
/**
* Get an instance of `Window` for the current window.
*
@@ -1446,6 +1484,32 @@ class Window {
})
}
/**
* Sets the taskbar progress state.
*
* #### Platform-specific
*
* - **Linux / macOS**: Progress bar is app-wide and not specific to this window.
* - **Linux**: Only supported desktop environments with `libunity` (e.g. GNOME).
*
* @example
* ```typescript
* import { getCurrent, ProgressBarStatus } from '@tauri-apps/api/window';
* await getCurrent().setProgressBar({
* status: ProgressBarStatus.Normal,
* progress: 50,
* });
* ```
*
* @return A promise indicating the success or failure of the operation.
*/
async setProgressBar(state: ProgressBarState): Promise<void> {
return invoke('plugin:window|set_progress_bar', {
label: this.label,
value: state
})
}
// Listeners
/**