Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions deno.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

107 changes: 107 additions & 0 deletions docs/NOTCH_API_USAGE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# Notch Size API Usage

## Commands Available

Two new Tauri commands have been added to retrieve notch information:

### 1. `get_notch_size`

Returns the notch dimensions including an estimated width and the actual height from safe area insets.

**Response Type:**
```typescript
interface NotchSize {
width: number; // Estimated notch width (approximately 12% of screen width)
height: number; // Actual notch height from safe area insets
top_inset: number; // Same as height, the top safe area inset
}
```

### 2. `get_safe_area_insets`

Returns all safe area insets for the main screen.

**Response Type:**
```typescript
interface SafeAreaInsets {
top: number; // Top inset (includes notch height if present)
bottom: number; // Bottom inset
left: number; // Left inset
right: number; // Right inset
}
```

## Usage Examples

### JavaScript/TypeScript (Tauri v2)

```typescript
import { invoke } from '@tauri-apps/api/core';

// Get notch size
async function getNotchSize() {
try {
const notchSize = await invoke<NotchSize>('get_notch_size');
console.log('Notch dimensions:', notchSize);
// Example output: { width: 220.8, height: 37, top_inset: 37 }
} catch (error) {
console.error('Failed to get notch size:', error);
}
}

// Get safe area insets
async function getSafeAreaInsets() {
try {
const insets = await invoke<SafeAreaInsets>('get_safe_area_insets');
console.log('Safe area insets:', insets);
// Example output: { top: 37, bottom: 0, left: 0, right: 0 }
} catch (error) {
console.error('Failed to get safe area insets:', error);
}
}

// Use the notch information to position UI elements
async function positionAroundNotch() {
const notchSize = await invoke<NotchSize>('get_notch_size');

if (notchSize.height > 0) {
// Device has a notch
console.log(`Notch detected: ${notchSize.width}px wide, ${notchSize.height}px tall`);

// Add padding to avoid the notch area
document.documentElement.style.setProperty('--notch-height', `${notchSize.height}px`);
document.documentElement.style.setProperty('--notch-width', `${notchSize.width}px`);
} else {
console.log('No notch detected');
}
}
```

### CSS Usage

```css
:root {
--notch-height: 0px;
--notch-width: 0px;
}

/* Avoid content under the notch */
.top-bar {
padding-top: var(--notch-height);
}

/* Position content to the left or right of the notch */
.left-section {
width: calc((100vw - var(--notch-width)) / 2);
}
```

## Notes

- The notch **height** is accurate and comes directly from `NSScreen.safeAreaInsets.top`
- The notch **width** is estimated at ~12% of screen width since macOS doesn't expose the exact notch width
- For typical MacBook Pros with notches:
- Height: ~32-37 pixels (at 2x scaling)
- Width: ~200-230 pixels (at 2x scaling)
- On devices without a notch, all values will be 0
- These commands must be called on the main thread (Tauri handles this automatically)
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-opener": "^2",
"tailwindcss": "^4.1.14",
"tauri-plugin-media-api": "^0.1.1",
"usehooks-ts": "^3.1.1",
"zustand": "^5.0.8"
},
"devDependencies": {
Expand Down
6 changes: 6 additions & 0 deletions src-tauri/capabilities/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@
}
],
"sidecar": false
},
{
"name": "osascript",
"cmd": "osascript",
"args": true,
"sidecar": false
}
]
}
Expand Down
71 changes: 71 additions & 0 deletions src-tauri/src/commands/get_notch_size.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
use objc2_app_kit::NSScreen;
use objc2_foundation::MainThreadMarker;
use serde::Serialize;

#[derive(Debug, Serialize)]
pub struct NotchSize {
pub width: f64,
pub height: f64,
pub top_inset: f64,
}

#[derive(Debug, Serialize)]
pub struct SafeAreaInsets {
pub top: f64,
pub bottom: f64,
pub left: f64,
pub right: f64,
}

#[tauri::command]
pub fn get_notch_size() -> Result<NotchSize, String> {
// Get the main thread marker (required for AppKit APIs)
let mtm = MainThreadMarker::new().ok_or("Must be called on main thread")?;

// Get the main screen
let screen = NSScreen::mainScreen(mtm).ok_or("Failed to get main screen")?;

// Get safe area insets (notch intrudes from the top)
let safe_area = screen.safeAreaInsets();

// Get the screen frame to calculate notch width
let frame = screen.frame();

// The notch typically spans a portion of the screen width
// For a more accurate width, we'd need to calculate the difference
// between the full width and the safe area, but the safe area
// only gives us top/bottom/left/right insets.

// The top inset represents the notch height
let notch_height = safe_area.top;

// For now, we'll estimate the notch width based on typical MacBook Pro dimensions
// In reality, the notch width isn't directly exposed by the API
// A more accurate method would be to measure the menu bar spacing
let notch_width = if notch_height > 0.0 {
// Typical notch width is around 200-230 pixels at 2x scale
frame.size.width * 0.12 // Approximately 12% of screen width
} else {
0.0
};

Ok(NotchSize {
width: notch_width,
height: notch_height,
top_inset: safe_area.top,
})
}

#[tauri::command]
pub fn get_safe_area_insets() -> Result<SafeAreaInsets, String> {
let mtm = MainThreadMarker::new().ok_or("Must be called on main thread")?;
let screen = NSScreen::mainScreen(mtm).ok_or("Failed to get main screen")?;
let safe_area = screen.safeAreaInsets();

Ok(SafeAreaInsets {
top: safe_area.top,
bottom: safe_area.bottom,
left: safe_area.left,
right: safe_area.right,
})
}
1 change: 1 addition & 0 deletions src-tauri/src/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pub mod get_app_icon;
pub mod get_notch_size;
pub mod set_window_behavior;
2 changes: 2 additions & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ pub fn run() {
.plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![
commands::get_app_icon::get_app_icon,
commands::get_notch_size::get_notch_size,
commands::get_notch_size::get_safe_area_insets,
commands::set_window_behavior::set_window_behavior
])
.setup(|app| {
Expand Down
6 changes: 5 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@ import './App.css'
import AeroSpace from './widgets/AeroSpace'
import { TimeWidget } from './widgets/Time'
import { Bar } from './components/Bar'
import { Island } from './components/Island'
import { SpotifyWidget } from './widgets/Spotify'

function App() {
return (
<main className='w-full h-[100vh]'>
<Bar left={<AeroSpace renameableWorkspace={true} />} right={<TimeWidget />} />
<Bar
left={<AeroSpace renameableWorkspace={true} />} right={<TimeWidget />} />
<SpotifyWidget />
</main>
)
}
Expand Down
89 changes: 89 additions & 0 deletions src/components/Island.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { useEffect, useRef, useState } from 'react'
import { invoke } from '@tauri-apps/api/core'
import { useOnClickOutside } from 'usehooks-ts'
import clsx from 'clsx'

interface NotchSize {
width: number
height: number
top_inset: number
}

const getNotchSize = async () => {
return await invoke<NotchSize>('get_notch_size')
}

export const Island = ({
children
}: {
children: React.ReactNode
}) => {
const [notchSize, setNotchSize] = useState<NotchSize | null>(null)
const [show, setShow] = useState(false)

useEffect(() => {
getNotchSize().then((notch) => {
setNotchSize(notch)
})
}, [])

useEffect(() => {
invoke('get_now_playing_info').then((media) => {
console.log(media)
})
}, [])

useEffect(() => {
if (show) {
invoke('set_window_behavior', {
alwaysOnTop: true,
alwaysOnBottom: false,
})
} else {
invoke('set_window_behavior', {
alwaysOnTop: false,
alwaysOnBottom: true
})
}
}, [show])

const ref = useRef<HTMLDivElement>(null)
useOnClickOutside(ref as any, () => setShow(false))

return <div className={clsx(
'fixed', 'top-[4px]', 'left-[25%]', 'w-0', 'overflow-visible',
'text-foreground'
)}>

<div className='whitespace-nowrap'>hello world</div>
</div>

// return (
// <div
// ref={ref}
// onClick={() => setShow(!show)}
// className={clsx(
// 'absolute text-foreground rounded-3xl p-2', show ? 'bg-[#000]' : 'bg-background/50',
// 'transition-all duration-300',
// 'overflow-hidden',
// )}

// style={{
// minWidth: show ? (notchSize?.width || 0) + 100 : notchSize?.width,
// height: show ? (notchSize?.height || 0) + 32 : notchSize?.height,
// top: 4,
// left: '50%',
// transform: 'translateX(-50%)',
// }}
// >
// {!show && <div>
// <div
// className='inline-block'
// style={{
// width: notchSize?.width || 0,
// }}
// />
// </div>}
// </div>
// )
}
Loading