[macOS] native sidebar blur effect (#239)

* wip rust bindings

* wip rust bindings

* attempt raw objc for window blur -- still nothing

* some minor blurring success...

* Fix macOS sidebar blur

* darken sidebar

* lock to dark theme

* remove commented, unused window-blur lines

* remove xcode user state

* ADD SWIFT WINDOW CODE

* refactor: use swift window code

* remove stupid cas warning

* add webview swift util for reload

* remove objc and cocoa

* remove old unused swift build fix

* simplify swift package fn calls

* enumify app theme

* fix main content view not expanding

* fix sidebar folder item layout

* fix swift version requirement

* fix landing package json

* landing tweaks

* sidebar style tweaks for macOS

Co-authored-by: maxichrome <maxichrome@users.noreply.github.com>
Co-authored-by: Jamie Pine <ijamespine@me.com>
This commit is contained in:
maxichrome 2022-06-15 15:53:42 -05:00 committed by GitHub
parent d7c070b7cb
commit 498da6a73e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 437 additions and 2154 deletions

View file

@ -15,6 +15,7 @@
"Roadmap",
"svgr",
"tailwindcss",
"titlebar",
"trivago",
"tsparticles",
"unlisten",

2170
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -7,11 +7,10 @@ license = ""
repository = "https://github.com/spacedriveapp/spacedrive"
default-run = "spacedrive"
edition = "2021"
build = "src/build.rs"
build = "build.rs"
[build-dependencies]
tauri-build = { version = "1.0.0-rc.5", features = [] }
swift-rs = "0.2.3"
[dependencies]
# Project dependencies
@ -25,8 +24,10 @@ window-shadows = "0.1.2"
# macOS system libs
[target.'cfg(target_os = "macos")'.dependencies]
cocoa = "0.24.0"
objc = "0.2.7"
swift-rs = { git = "https://github.com/Brendonovich/swift-rs.git", branch = "autorelease" }
[target.'cfg(target_os = "macos")'.build-dependencies]
swift-rs = { git = "https://github.com/Brendonovich/swift-rs.git", branch = "autorelease", features = ["build"] }
[features]
default = [ "custom-protocol" ]

View file

@ -0,0 +1,8 @@
use swift_rs::build::{link_swift, link_swift_package};
fn main() {
link_swift();
link_swift_package("sd-desktop-macos", "./native/macos/");
tauri_build::build();
}

View file

@ -0,0 +1,9 @@
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/
DerivedData/
.swiftpm/config/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc

View file

@ -0,0 +1,16 @@
{
"object": {
"pins": [
{
"package": "SwiftRs",
"repositoryURL": "https://github.com/brendonovich/swift-rs.git",
"state": {
"branch": "autorelease",
"revision": "fe5a1c2f668e6bade43d6f56e2530f110055cee9",
"version": null
}
}
]
},
"version": 1
}

View file

@ -0,0 +1,32 @@
// swift-tools-version: 5.5
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "sd-desktop-macos",
platforms: [
.macOS(.v11)
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "sd-desktop-macos",
type: .static,
targets: ["sd-desktop-macos"]
),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
.package(url: "https://github.com/brendonovich/swift-rs.git", branch: "autorelease"),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "sd-desktop-macos",
dependencies: [
.product(name: "SwiftRs", package: "swift-rs")
]),
]
)

View file

@ -0,0 +1,8 @@
import WebKit
@_cdecl("reload_webview")
public func reloadWebview(webview: WKWebView) -> () {
webview.window!.orderOut(webview);
webview.reload();
webview.window!.makeKey();
}

View file

@ -0,0 +1,66 @@
import AppKit
@objc
public enum AppThemeType: Int {
case light = 0;
case dark = 1;
}
@_cdecl("lock_app_theme")
public func lockAppTheme(themeType: AppThemeType) {
var theme: NSAppearance;
switch themeType {
case .dark:
theme = NSAppearance(named: .darkAqua)!;
case .light:
theme = NSAppearance(named: .aqua)!;
}
NSApp.appearance = theme;
}
@_cdecl("blur_window_background")
public func blurWindowBackground(window: NSWindow) {
let windowContent = window.contentView!;
let blurryView = NSVisualEffectView();
blurryView.material = .sidebar;
blurryView.state = .followsWindowActiveState;
blurryView.blendingMode = .behindWindow;
blurryView.wantsLayer = true;
window.contentView = blurryView;
blurryView.addSubview(windowContent);
}
@_cdecl("set_invisible_toolbar")
public func setInvisibleToolbar(window: NSWindow, hasToolbar: Bool) {
if !hasToolbar {
window.toolbar = nil;
return;
}
let toolbar = NSToolbar(identifier: "window_invisible_toolbar");
toolbar.showsBaselineSeparator = false;
window.toolbar = toolbar;
}
@_cdecl("set_titlebar_style")
public func setTitlebarStyle(window: NSWindow, transparent: Bool, large: Bool) {
var styleMask = window.styleMask;
if transparent && large {
styleMask.insert(.unifiedTitleAndToolbar);
}
window.styleMask = styleMask;
if large {
setInvisibleToolbar(window: window, hasToolbar: true);
}
window.titleVisibility = transparent ? .hidden : .visible;
window.titlebarAppearsTransparent = transparent;
}

View file

@ -1,11 +0,0 @@
// use swift_rs::build_utils::{link_swift, link_swift_package};
fn main() {
// HOTFIX: compile the swift code for arm64
// std::env::set_var("CARGO_CFG_TARGET_ARCH", "arm64");
// link_swift();
// link_swift_package("swift-lib", "../../../packages/macos/");
tauri_build::build();
}

View file

@ -0,0 +1,7 @@
mod native;
mod window;
pub use window::*;
mod webview;
pub use webview::*;

View file

@ -0,0 +1,3 @@
use std::ffi::c_void;
pub type NSObject = *mut c_void;

View file

@ -0,0 +1,4 @@
use super::native::NSObject;
use swift_rs::*;
pub_swift_fn!(reload_webview(webview: NSObject));

View file

@ -0,0 +1,17 @@
use super::native::NSObject;
use swift_rs::*;
pub_swift_fn!(lock_app_theme(theme_type: Int));
pub_swift_fn!(blur_window_background(window: NSObject));
pub_swift_fn!(set_invisible_toolbar(window: NSObject, shown: Bool));
pub_swift_fn!(set_titlebar_style(
window: NSObject,
transparent: Bool,
large: Bool
));
#[allow(dead_code)]
pub enum AppThemeType {
Light = 0 as Int,
Dark = 1 as Int,
}

View file

@ -1,13 +1,13 @@
use std::time::{Duration, Instant};
use macos::AppThemeType;
use sdcore::{ClientCommand, ClientQuery, CoreController, CoreEvent, CoreResponse, Node};
use tauri::api::path;
use tauri::Manager;
#[cfg(target_os = "macos")]
mod macos;
mod menu;
mod window;
use window::WindowExt;
#[tauri::command(async)]
async fn client_query_transport(
@ -42,13 +42,6 @@ async fn app_ready(app_handle: tauri::AppHandle) {
let window = app_handle.get_window("main").unwrap();
window.show().unwrap();
#[cfg(target_os = "macos")]
{
std::thread::sleep(std::time::Duration::from_millis(1000));
println!("fixing shadow for, {:?}", window.ns_window().unwrap());
window.fix_shadow();
}
}
#[tokio::main]
@ -71,6 +64,11 @@ async fn main() {
.setup(|app| {
let app = app.handle();
#[cfg(target_os = "macos")]
{
macos::lock_app_theme(AppThemeType::Dark as _);
}
app.windows().iter().for_each(|(_, window)| {
window.hide().unwrap();
@ -78,7 +76,13 @@ async fn main() {
window.set_decorations(true).unwrap();
#[cfg(target_os = "macos")]
window.set_transparent_titlebar(true, true);
{
use macos::*;
let window = window.ns_window().unwrap();
set_titlebar_style(window, true, true);
blur_window_background(window);
}
});
// core event transport
@ -104,7 +108,6 @@ async fn main() {
Ok(())
})
.on_menu_event(|event| menu::handle_menu_event(event))
.on_window_event(|event| window::handle_window_event(event))
.invoke_handler(tauri::generate_handler![
client_query_transport,
client_command_transport,

View file

@ -1,4 +1,4 @@
use std::{env::consts, ffi::c_void};
use std::env::consts;
use tauri::{AboutMetadata, CustomMenuItem, Menu, MenuItem, Submenu, WindowMenuEvent, Wry};
@ -95,10 +95,11 @@ pub(crate) fn handle_menu_event(event: WindowMenuEvent<Wry>) {
.window()
.with_webview(|webview| {
#[cfg(target_os = "macos")]
unsafe {
use objc::{msg_send, sel, sel_impl};
let _result: c_void = msg_send![webview.inner(), reload];
};
{
use crate::macos::reload_webview;
reload_webview(webview.inner() as _);
}
})
.unwrap();
}

View file

@ -1,93 +0,0 @@
use tauri::{GlobalWindowEvent, Runtime, Window, Wry};
pub(crate) fn handle_window_event(event: GlobalWindowEvent<Wry>) {
match event.event() {
_ => {}
}
}
pub trait WindowExt {
#[cfg(target_os = "macos")]
fn set_toolbar(&self, shown: bool);
#[cfg(target_os = "macos")]
fn set_transparent_titlebar(&self, transparent: bool, large: bool);
#[cfg(target_os = "macos")]
fn fix_shadow(&self);
}
impl<R: Runtime> WindowExt for Window<R> {
#[cfg(target_os = "macos")]
fn set_toolbar(&self, shown: bool) {
use cocoa::{
appkit::{NSToolbar, NSWindow},
base::{nil, NO},
foundation::NSString,
};
unsafe {
let id = self.ns_window().unwrap() as cocoa::base::id;
if shown {
let toolbar =
NSToolbar::alloc(nil).initWithIdentifier_(NSString::alloc(nil).init_str("wat"));
toolbar.setShowsBaselineSeparator_(NO);
id.setToolbar_(toolbar);
} else {
id.setToolbar_(nil);
}
}
}
#[cfg(target_os = "macos")]
fn set_transparent_titlebar(&self, transparent: bool, large: bool) {
use cocoa::{
appkit::{NSWindow, NSWindowStyleMask, NSWindowTitleVisibility},
base::{NO, YES},
};
unsafe {
let id = self.ns_window().unwrap() as cocoa::base::id;
let mut style_mask = id.styleMask();
// println!("existing style mask, {:#?}", style_mask);
style_mask.set(
NSWindowStyleMask::NSFullSizeContentViewWindowMask,
transparent,
);
style_mask.set(
NSWindowStyleMask::NSTexturedBackgroundWindowMask,
transparent,
);
style_mask.set(
NSWindowStyleMask::NSUnifiedTitleAndToolbarWindowMask,
transparent && large,
);
id.setStyleMask_(style_mask);
if large {
self.set_toolbar(true);
}
id.setTitleVisibility_(if transparent {
NSWindowTitleVisibility::NSWindowTitleHidden
} else {
NSWindowTitleVisibility::NSWindowTitleVisible
});
id.setTitlebarAppearsTransparent_(if transparent { YES } else { NO });
}
}
#[cfg(target_os = "macos")]
fn fix_shadow(&self) {
use cocoa::appkit::NSWindow;
unsafe {
let id = self.ns_window().unwrap() as cocoa::base::id;
println!("recomputing shadow for window {:?}", id.title());
id.invalidateShadow();
}
}
}

View file

@ -1,7 +1,10 @@
{
"name": "@sd/landing",
"private": true,
"version": "0.0.0",
"scripts": {
"dev": "npm run server",
"prod": "npm run build && npm run server:prod",
"dev": "pnpm run server",
"prod": "pnpm run build && pnpm run server:prod",
"vercel-build": "./vercel/deploy.sh",
"build": "vite build && vite build --ssr && vite-plugin-ssr prerender",
"server": "ts-node ./server",

View file

@ -83,7 +83,7 @@ function Page() {
<div className="mt-24 lg:mt-5" />
<NewBanner
headline="Spacedrive raises $2M led by OSS Capital"
href="https://spacedrive.com/blog/spacedrive-funding-announcement"
href="/blog/spacedrive-funding-announcement"
link="Read post"
/>
{unsubscribedFromWaitlist && (
@ -97,7 +97,7 @@ function Page() {
</div>
)}
<h1 className="z-30 px-2 mb-3 text-4xl text-white font-black leading-tight text-center fade-in-heading md:text-6xl">
<h1 className="z-30 px-2 mb-3 text-4xl font-black leading-tight text-center text-white fade-in-heading md:text-6xl">
A file explorer from the future.
</h1>
<p className="z-30 max-w-4xl mt-1 mb-8 text-center animation-delay-1 fade-in-heading text-md lg:text-lg leading-2 lg:leading-8 text-gray-450">

View file

@ -48,7 +48,7 @@ html {
.fade-in-whats-new {
animation: fadeInDown 1s forwards;
animation-delay: 1.5s;
animation-delay: 600ms;
opacity: 0;
}

View file

@ -156,7 +156,7 @@ impl Job for FileIdentifierJob {
})
.await?;
let remaining = count_orphan_file_paths(&ctx.core_ctx, location.id.into()).await?;
let _remaining = count_orphan_file_paths(&ctx.core_ctx, location.id.into()).await?;
Ok(())
}

View file

@ -75,7 +75,7 @@ function AppLayout() {
return false;
}}
className={clsx(
'flex flex-row h-screen overflow-hidden text-gray-900 bg-white select-none dark:text-white dark:bg-gray-650',
'flex flex-row h-screen overflow-hidden text-gray-900 select-none dark:text-white',
isWindowRounded && 'rounded-xl',
hasWindowBorder && 'border border-gray-200 dark:border-gray-500'
)}
@ -84,7 +84,7 @@ function AppLayout() {
<div className="flex flex-col w-full min-h-full">
{/* <TopBar /> */}
<div className="relative flex w-full">
<div className="relative flex w-full min-h-full bg-white dark:bg-gray-650">
<Outlet />
</div>
</div>

View file

@ -21,7 +21,7 @@ export const SidebarLink = (props: NavLinkProps & { children: React.ReactNode })
{({ isActive }) => (
<span
className={clsx(
'max-w mb-[2px] text-gray-550 dark:text-gray-150 rounded px-2 py-1 flex flex-row flex-grow items-center font-medium hover:bg-gray-100 dark:hover:bg-gray-600 text-sm',
'max-w mb-[2px] text-gray-550 dark:text-gray-150 rounded px-2 py-1 flex flex-row flex-grow items-center font-medium text-sm',
{
'!bg-primary !text-white hover:bg-primary dark:hover:bg-primary': isActive
},
@ -69,6 +69,10 @@ export function MacWindowControls() {
);
}
// cute little helper to decrease code clutter
const macOnly = (platform: string | undefined, classnames: string) =>
platform === 'macOS' ? classnames : '';
export const Sidebar: React.FC<SidebarProps> = (props) => {
const { isExperimental } = useNodeStore();
@ -87,7 +91,14 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
];
return (
<div className="flex flex-col flex-grow-0 flex-shrink-0 w-48 min-h-full px-2.5 overflow-x-hidden overflow-y-scroll border-r border-gray-100 no-scrollbar bg-gray-50 dark:bg-gray-850 dark:border-gray-600">
<div
className={clsx(
'flex flex-col flex-grow-0 flex-shrink-0 w-48 min-h-full px-2.5 overflow-x-hidden overflow-y-scroll border-r border-gray-100 no-scrollbar bg-gray-50 dark:bg-gray-850 dark:border-gray-600',
{
'dark:!bg-opacity-40': appProps?.platform === 'macOS'
}
)}
>
{appProps?.platform === 'browser' && window.location.search.includes('showControls') ? (
<MacWindowControls />
) : null}
@ -96,19 +107,32 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
<Dropdown
buttonProps={{
justifyLeft: true,
className: `flex w-full text-left max-w-full mb-1 mt-1 -mr-0.5 shadow-xs rounded
className: clsx(
`flex w-full text-left max-w-full mb-1 mt-1 -mr-0.5 shadow-xs rounded
!bg-gray-50
border-gray-150
hover:!bg-gray-1000
dark:!bg-gray-550
dark:hover:!bg-gray-550
dark:!border-gray-550
dark:hover:!border-gray-500`,
dark:!bg-gray-500
dark:hover:!bg-gray-500
dark:!border-gray-550
dark:hover:!border-gray-500
`,
appProps?.platform === 'macOS' &&
'dark:!bg-opacity-40 dark:hover:!bg-opacity-70 dark:!border-[#333949] dark:hover:!border-[#394052]'
),
variant: 'gray'
}}
// buttonIcon={<Book weight="bold" className="w-4 h-4 mt-0.5 mr-1" />}
// to support the transparent sidebar on macOS we use slightly adjusted styles
itemsClassName={macOnly(appProps?.platform, 'dark:bg-gray-800 dark:divide-gray-600')}
itemButtonClassName={macOnly(
appProps?.platform,
'dark:hover:bg-gray-550 dark:hover:bg-opacity-50'
)}
// this shouldn't default to "My Library", it is only this way for landing demo
// TODO: implement demo mode for the sidebar and show loading indicator instead of "My Library"
buttonText={clientState?.node_name || 'My Library'}
items={[
[
@ -146,11 +170,6 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
) : (
<></>
)}
{/* <SidebarLink to="explorer">
<Icon component={MonitorPlay} />
Explorer
</SidebarLink> */}
</div>
<div>
<Heading>Locations</Heading>
@ -158,7 +177,7 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
return (
<div key={index} className="flex flex-row items-center">
<NavLink
className="'relative w-full group'"
className="relative w-full group"
to={{
pathname: `explorer/${location.id}`
}}
@ -166,18 +185,18 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
{({ isActive }) => (
<span
className={clsx(
'max-w mb-[2px] text-gray-550 dark:text-gray-150 rounded px-2 py-1 flex flex-row flex-grow items-center hover:bg-gray-100 dark:hover:bg-gray-600 text-sm',
'max-w mb-[2px] text-gray-550 dark:text-gray-150 rounded px-2 py-1 gap-2 flex flex-row flex-grow items-center truncate text-sm',
{
'!bg-primary !text-white hover:bg-primary dark:hover:bg-primary': isActive
}
)}
>
<div className="w-[18px] mr-2 -mt-0.5">
<Folder className={clsx(!isActive && 'hidden')} white />
<Folder className={clsx(isActive && 'hidden')} />
<div className="-mt-0.5 flex-grow-0 flex-shrink-0">
<Folder size={18} className={clsx(!isActive && 'hidden')} white />
<Folder size={18} className={clsx(isActive && 'hidden')} />
</div>
{location.name}
<div className="flex-grow" />
<span className="flex-grow flex-shrink-0">{location.name}</span>
</span>
)}
</NavLink>
@ -191,7 +210,7 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
createLocation({ path: result });
});
}}
className="w-full px-2 py-1.5 mt-1 text-xs font-bold text-center text-gray-400 dark:text-gray-500 border border-dashed rounded border-transparent cursor-normal border-gray-350 dark:border-gray-550 hover:dark:border-gray-500 transition"
className="w-full px-2 py-1.5 mt-1 text-xs font-bold text-center text-gray-400 dark:text-gray-400 border border-dashed rounded border-transparent cursor-normal border-gray-350 dark:border-gray-450 hover:dark:border-gray-400 transition"
>
Add Location
</button>

View file

@ -13,12 +13,21 @@ interface FolderProps {
* Render a white folder icon
*/
white?: boolean;
/**
* The size of the icon to show -- uniform width and height
*/
size?: number;
}
export function Folder(props: FolderProps) {
const { size = 24 } = props;
return (
<img
className={props.className}
width={size}
height={size}
src={props.white ? folderWhiteSvg : folderSvg}
alt="Folder icon"
/>

View file

@ -153,7 +153,7 @@ export const OverviewScreen = () => {
<div className="flex flex-col w-full h-screen overflow-x-hidden custom-scroll page-scroll">
<div data-tauri-drag-region className="flex flex-shrink-0 w-full h-5" />
{/* PAGE */}
<div className="flex flex-col w-full h-screen px-3">
<div className="flex flex-col w-full h-screen px-4">
{/* STAT HEADER */}
<div className="flex w-full">
{/* STAT CONTAINER */}

View file

@ -20,6 +20,7 @@ export interface DropdownProps {
buttonIcon?: any;
className?: string;
itemsClassName?: string;
itemButtonClassName?: string;
}
export const Dropdown: React.FC<DropdownProps> = (props) => {
@ -64,7 +65,8 @@ export const Dropdown: React.FC<DropdownProps> = (props) => {
{
'bg-gray-300 dark:!bg-gray-500 dark:hover:bg-gray-500': button.selected
// 'text-gray-900 dark:text-gray-200': !active
}
},
props.itemButtonClassName
)}
>
{button.icon && (