Skip to content

Commit

Permalink
feat(macOS): support progress bar on dock (#766)
Browse files Browse the repository at this point in the history
Co-authored-by: Lucas Nogueira <[email protected]>
  • Loading branch information
pewsheen and lucasfernog authored Jul 13, 2023
1 parent 4233d26 commit 3b7e0d9
Show file tree
Hide file tree
Showing 9 changed files with 276 additions and 11 deletions.
2 changes: 1 addition & 1 deletion .changes/implementTaskbarProgress.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
"tao": minor
---

Added APIs for setting progress bars for the application icon on Linux (Unity only) and for specific window on Windows.
Added APIs for setting progress bars for the application icon on Linux (Unity only) and macOS, along with progress indicator for specific window on Windows.
89 changes: 89 additions & 0 deletions examples/progress_bar.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Copyright 2014-2021 The winit contributors
// Copyright 2021-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0

use tao::{
event::{ElementState, Event, KeyEvent, WindowEvent},
event_loop::{ControlFlow, EventLoop},
keyboard::{Key, ModifiersState},
window::{ProgressBarState, ProgressState, WindowBuilder},
};

#[allow(clippy::single_match)]
fn main() {
env_logger::init();
let event_loop = EventLoop::new();

let window = WindowBuilder::new().build(&event_loop).unwrap();

let mut modifiers = ModifiersState::default();

eprintln!("Key mappings:");
eprintln!(" [1-5]: Set progress to [0%, 25%, 50%, 75%, 100%]");
eprintln!(" Ctrl+1: Set state to None");
eprintln!(" Ctrl+2: Set state to Normal");
eprintln!(" Ctrl+3: Set state to Indeterminate");
eprintln!(" Ctrl+4: Set state to Paused");
eprintln!(" Ctrl+5: Set state to Error");

event_loop.run(move |event, _, control_flow| {
*control_flow = ControlFlow::Wait;

match event {
Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} => *control_flow = ControlFlow::Exit,
Event::WindowEvent { event, .. } => match event {
WindowEvent::ModifiersChanged(new_state) => {
modifiers = new_state;
}
WindowEvent::KeyboardInput {
event:
KeyEvent {
logical_key: Key::Character(key_str),
state: ElementState::Released,
..
},
..
} => {
if modifiers.is_empty() {
let mut progress: u64 = 0;
match key_str {
"1" => progress = 0,
"2" => progress = 25,
"3" => progress = 50,
"4" => progress = 75,
"5" => progress = 100,
_ => {}
}

window.set_progress_bar(ProgressBarState {
progress: Some(progress),
state: Some(ProgressState::Normal),
unity_uri: None,
});
} else if modifiers.control_key() {
let mut state = ProgressState::None;
match key_str {
"1" => state = ProgressState::None,
"2" => state = ProgressState::Normal,
"3" => state = ProgressState::Indeterminate,
"4" => state = ProgressState::Paused,
"5" => state = ProgressState::Error,
_ => {}
}

window.set_progress_bar(ProgressBarState {
progress: None,
state: Some(state),
unity_uri: None,
});
}
}
_ => {}
},
_ => {}
}
});
}
3 changes: 1 addition & 2 deletions src/event_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -261,11 +261,10 @@ impl<T> EventLoopWindowTarget<T> {
///
/// - **Windows:** Unsupported. Use the Progress Bar Function Available in Window (Windows can have different progress bars for different window)
/// - **Linux:** Only supported desktop environments with `libunity` (e.g. GNOME).
/// - **macOS:** Unimplemented.
/// - **iOS / Android:** Unsupported.
#[inline]
pub fn set_progress_bar(&self, _progress: ProgressBarState) {
#[cfg(target_os = "linux")]
#[cfg(any(target_os = "linux", target_os = "macos"))]
self.p.set_progress_bar(_progress)
}
}
Expand Down
7 changes: 7 additions & 0 deletions src/platform_impl/macos/event_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ use crate::{
observer::*,
util::{self, IdRef},
},
platform_impl::set_progress_indicator,
window::ProgressBarState,
};

#[derive(Default)]
Expand Down Expand Up @@ -111,6 +113,11 @@ impl<T: 'static> EventLoopWindowTarget<T> {
Err(ExternalError::Os(os_error!(super::OsError::CGError(0))))
}
}

#[inline]
pub fn set_progress_bar(&self, progress: ProgressBarState) {
set_progress_indicator(progress);
}
}

pub struct EventLoop<T: 'static> {
Expand Down
2 changes: 2 additions & 0 deletions src/platform_impl/macos/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ mod keycode;
mod menu;
mod monitor;
mod observer;
mod progress_bar;
#[cfg(feature = "tray")]
mod system_tray;
mod util;
Expand All @@ -38,6 +39,7 @@ pub use self::{
keycode::{keycode_from_scancode, keycode_to_scancode},
menu::{Menu, MenuItemAttributes},
monitor::{MonitorHandle, VideoMode},
progress_bar::set_progress_indicator,
window::{Id as WindowId, Parent, PlatformSpecificWindowBuilderAttributes, UnownedWindow},
};
use crate::{
Expand Down
162 changes: 162 additions & 0 deletions src/platform_impl/macos/progress_bar.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
use std::sync::Once;

use cocoa::{
base::{id, nil},
foundation::{NSArray, NSPoint, NSRect, NSSize},
};
use objc::{
declare::ClassDecl,
runtime::{Class, Object, Sel, NO},
};

use crate::window::{ProgressBarState, ProgressState};

/// Set progress indicator in the Dock.
pub fn set_progress_indicator(progress_state: ProgressBarState) {
unsafe {
let ns_app: id = msg_send![class!(NSApplication), sharedApplication];
let dock_tile: id = msg_send![ns_app, dockTile];
if dock_tile == nil {
return;
}

// check progress indicator is already set or create new one
let progress_indicator: id = get_exist_progress_indicator(dock_tile)
.unwrap_or_else(|| create_progress_indicator(ns_app, dock_tile));

// set progress indicator state
if let Some(progress) = progress_state.progress {
let progress = progress.clamp(0, 100) as f64;
let _: () = msg_send![progress_indicator, setDoubleValue: progress];
let _: () = msg_send![progress_indicator, setHidden: NO];
}
if let Some(state) = progress_state.state {
(*progress_indicator).set_ivar("state", state as u8);
let _: () = msg_send![
progress_indicator,
setHidden: matches!(state, ProgressState::None)
];
}

let _: () = msg_send![dock_tile, display];
}
}

fn create_progress_indicator(ns_app: id, dock_tile: id) -> id {
unsafe {
let mut image_view: id = msg_send![dock_tile, contentView];
if image_view == nil {
// create new dock tile view with current app icon
let app_icon_image: id = msg_send![ns_app, applicationIconImage];
image_view = msg_send![class!(NSImageView), imageViewWithImage: app_icon_image];
let _: () = msg_send![dock_tile, setContentView: image_view];
}

// create custom progress indicator
let dock_tile_size: NSSize = msg_send![dock_tile, size];
let frame = NSRect::new(
NSPoint::new(0.0, 0.0),
NSSize::new(dock_tile_size.width, 15.0),
);
let progress_class = create_progress_indicator_class();
let progress_indicator: id = msg_send![progress_class, alloc];
let progress_indicator: id = msg_send![progress_indicator, initWithFrame: frame];
let _: () = msg_send![progress_indicator, autorelease];

// set progress indicator to the dock tile
let _: () = msg_send![image_view, addSubview: progress_indicator];

progress_indicator
}
}

fn get_exist_progress_indicator(dock_tile: id) -> Option<id> {
unsafe {
let content_view: id = msg_send![dock_tile, contentView];
if content_view == nil {
return None;
}
let subviews: id /* NSArray */ = msg_send![content_view, subviews];
if subviews == nil {
return None;
}

for idx in 0..subviews.count() {
let subview: id = msg_send![subviews, objectAtIndex: idx];

let is_progress_indicator: bool =
msg_send![subview, isKindOfClass: class!(NSProgressIndicator)];
if is_progress_indicator {
return Some(subview);
}
}
}
None
}

fn create_progress_indicator_class() -> *const Class {
static mut APP_CLASS: *const Class = 0 as *const Class;
static INIT: Once = Once::new();

INIT.call_once(|| unsafe {
let superclass = class!(NSProgressIndicator);
let mut decl = ClassDecl::new("TaoProgressIndicator", superclass).unwrap();

decl.add_method(
sel!(drawRect:),
draw_progress_bar as extern "C" fn(&Object, _, NSRect),
);

// progress bar states, follows ProgressState
decl.add_ivar::<u8>("state");

APP_CLASS = decl.register();
});

unsafe { APP_CLASS }
}

extern "C" fn draw_progress_bar(this: &Object, _: Sel, rect: NSRect) {
unsafe {
let bar = NSRect::new(
NSPoint { x: 0.0, y: 4.0 },
NSSize {
width: rect.size.width,
height: 8.0,
},
);
let bar_inner = bar.inset(0.5, 0.5);
let mut bar_progress = bar.inset(1.0, 1.0);

// set progress width
let current_progress: f64 = msg_send![this, doubleValue];
let normalized_progress: f64 = (current_progress / 100.0).clamp(0.0, 1.0);
bar_progress.size.width *= normalized_progress;

// draw outer bar
let bg_color: id = msg_send![class!(NSColor), colorWithWhite:1.0 alpha:0.05];
let _: () = msg_send![bg_color, set];
draw_rounded_rect(bar);
// draw inner bar
draw_rounded_rect(bar_inner);

// draw progress
let state: u8 = *(this.get_ivar("state"));
let progress_color: id = match state {
x if x == ProgressState::Paused as u8 => msg_send![class!(NSColor), systemYellowColor],
x if x == ProgressState::Error as u8 => msg_send![class!(NSColor), systemRedColor],
_ => msg_send![class!(NSColor), systemBlueColor],
};
let _: () = msg_send![progress_color, set];
draw_rounded_rect(bar_progress);
}
}

fn draw_rounded_rect(rect: NSRect) {
unsafe {
let raduis = rect.size.height / 2.0;
let bezier_path: id =
msg_send![class!(NSBezierPath), bezierPathWithRoundedRect:rect xRadius:raduis yRadius:raduis];
let _: () = msg_send![bezier_path, fill];
}
}
9 changes: 7 additions & 2 deletions src/platform_impl/macos/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,10 @@ use crate::{
window_delegate::new_delegate,
OsError,
},
platform_impl::set_progress_indicator,
window::{
CursorIcon, Fullscreen, Theme, UserAttentionType, WindowAttributes, WindowId as RootWindowId,
WindowSizeConstraints,
CursorIcon, Fullscreen, ProgressBarState, Theme, UserAttentionType, WindowAttributes,
WindowId as RootWindowId, WindowSizeConstraints,
},
};
use cocoa::{
Expand Down Expand Up @@ -1426,6 +1427,10 @@ impl UnownedWindow {
self.ns_window.setCollectionBehavior_(collection_behavior)
}
}

pub fn set_progress_bar(&self, progress: ProgressBarState) {
set_progress_indicator(progress);
}
}

impl WindowExtMacOS for UnownedWindow {
Expand Down
2 changes: 1 addition & 1 deletion src/platform_impl/windows/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -907,7 +907,7 @@ impl Window {
let taskbar_state = {
match state {
ProgressState::None => 0,
ProgressState::Intermediate => 1,
ProgressState::Indeterminate => 1,
ProgressState::Normal => 2,
ProgressState::Error => 3,
ProgressState::Paused => 4,
Expand Down
11 changes: 6 additions & 5 deletions src/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@ use crate::{
pub use crate::icon::{BadIcon, Icon};

/// Progress State
#[derive(Debug, Clone, Copy)]
pub enum ProgressState {
None,
Normal,
/// **Treated as Normal in linux**
Intermediate,
/// **Treated as Normal in linux and macOS**
Indeterminate,
/// **Treated as Normal in linux**
Paused,
/// **Treated as Normal in linux**
Expand Down Expand Up @@ -1084,8 +1085,7 @@ impl Window {
///
/// ## Platform-specific
///
/// - **Linux**: Progress bar is app-wide and not specific to this window. Only supported desktop environments with `libunity` (e.g. GNOME).
/// - **macOS**: Unimplemented.
/// - **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.
#[inline]
pub fn set_progress_bar(&self, progress: ProgressBarState) {
Expand All @@ -1095,7 +1095,8 @@ impl Window {
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
target_os = "openbsd",
target_os = "macos",
))]
self.window.set_progress_bar(progress)
}
Expand Down

0 comments on commit 3b7e0d9

Please sign in to comment.