Skip to content

Commit 193a9d2

Browse files
committed
feat(stage-tamagotchi): window & display utils
1 parent cd67a70 commit 193a9d2

File tree

3 files changed

+248
-0
lines changed

3 files changed

+248
-0
lines changed
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import type { Rectangle } from 'electron'
2+
3+
import { describe, expect, it } from 'vitest'
4+
5+
import { heightFrom, mapForBreakpoints, widthFrom } from './display'
6+
7+
describe('mapForBreakpoints', () => {
8+
it('should return the correct size based on breakpoints', () => {
9+
const val = mapForBreakpoints(800, { sm: 100, md: 200, lg: 300 })
10+
expect(val).toBe(200)
11+
})
12+
13+
it('it should fallback to nearest smaller breakpoint', () => {
14+
const val = mapForBreakpoints(1024, { sm: 100, md: 200 }) // expected to be lg
15+
expect(val).toBe(200)
16+
})
17+
18+
it('it should return the largest supplied size if bounds exceed all breakpoints', () => {
19+
const val1 = mapForBreakpoints(2000, { sm: 100, md: 200 }) // expected to be lg
20+
expect(val1).toBe(200)
21+
22+
const val2 = mapForBreakpoints(2000, { 'sm': 100, 'md': 200, '2xl': 500 }) // expected to be lg
23+
expect(val2).toBe(500)
24+
})
25+
})
26+
27+
describe('widthFrom', () => {
28+
it('should return width based on percentage', () => {
29+
expect(widthFrom({ width: 1000 } as Rectangle, { percentage: 0.5 })).toBe(500)
30+
})
31+
32+
it('should return width based on fixed value', () => {
33+
expect(widthFrom({ width: 1000 } as Rectangle, 300)).toBe(300)
34+
})
35+
36+
it('should respect min constraint', () => {
37+
expect(widthFrom({ width: 1000 } as Rectangle, { percentage: 0.1, min: 200 })).toBe(200)
38+
expect(widthFrom({ width: 1000 } as Rectangle, { actual: 150, min: 200 })).toBe(200)
39+
expect(widthFrom({ width: 1000 } as Rectangle, { actual: 250, min: 200 })).toBe(250)
40+
})
41+
42+
it('should respect max constraint', () => {
43+
expect(widthFrom({ width: 1000 } as Rectangle, { percentage: 0.5, max: 400 })).toBe(400)
44+
expect(widthFrom({ width: 1000 } as Rectangle, { actual: 450, max: 400 })).toBe(400)
45+
expect(widthFrom({ width: 1000 } as Rectangle, { actual: 350, max: 400 })).toBe(350)
46+
})
47+
})
48+
49+
describe('heightFrom', () => {
50+
it('should return height based on percentage', () => {
51+
expect(heightFrom({ height: 1000 } as Rectangle, { percentage: 0.5 })).toBe(500)
52+
})
53+
54+
it('should return height based on fixed value', () => {
55+
expect(heightFrom({ height: 1000 } as Rectangle, 300)).toBe(300)
56+
})
57+
58+
it('should respect min constraint', () => {
59+
expect(heightFrom({ height: 1000 } as Rectangle, { percentage: 0.1, min: 200 })).toBe(200)
60+
expect(heightFrom({ height: 1000 } as Rectangle, { actual: 150, min: 200 })).toBe(200)
61+
expect(heightFrom({ height: 1000 } as Rectangle, { actual: 250, min: 200 })).toBe(250)
62+
})
63+
64+
it('should respect max constraint', () => {
65+
expect(heightFrom({ height: 1000 } as Rectangle, { percentage: 0.5, max: 400 })).toBe(400)
66+
expect(heightFrom({ height: 1000 } as Rectangle, { actual: 450, max: 400 })).toBe(400)
67+
expect(heightFrom({ height: 1000 } as Rectangle, { actual: 350, max: 400 })).toBe(350)
68+
})
69+
})
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import type { BrowserWindow, Rectangle } from 'electron'
2+
3+
import { screen } from 'electron'
4+
5+
export function currentDisplayBounds(window: BrowserWindow) {
6+
const bounds = window.getBounds()
7+
const nearbyDisplay = screen.getDisplayMatching(bounds)
8+
9+
return nearbyDisplay.bounds
10+
}
11+
12+
interface SizeActual { actual: number }
13+
interface SizePercentage { percentage: number }
14+
type Size = SizeActual | SizePercentage | number
15+
16+
function evaluateSize(basedOn: number, size: Size) {
17+
if (typeof size === 'number') {
18+
return size
19+
}
20+
if ('actual' in size) {
21+
return size.actual
22+
}
23+
24+
return Math.floor(basedOn * size.percentage)
25+
}
26+
27+
/**
28+
* Breakpoint prefix Minimum width CSS
29+
* sm 40rem (640px) @media (width >= 40rem) { ... }
30+
* md 48rem (768px) @media (width >= 48rem) { ... }
31+
* lg 64rem (1024px) @media (width >= 64rem) { ... }
32+
* xl 80rem (1280px) @media (width >= 80rem) { ... }
33+
* 2xl 96rem (1536px) @media (width >= 96rem) { ... }
34+
*
35+
* Additional to tailwindcss defaults:
36+
* 3xl 112rem (1792px) @media (width >= 112rem) { ... }
37+
* 4xl 128rem (2048px) @media (width >= 128rem) { ... }
38+
* 5xl 144rem (2304px) @media (width >= 144rem) { ... }
39+
* 6xl 160rem (2560px) @media (width >= 160rem) { ... }
40+
* 7xl 176rem (2816px) @media (width >= 176rem) { ... }
41+
* 8xl 192rem (3072px) @media (width >= 192rem) { ... }
42+
* 9xl 208rem (3328px) @media (width >= 208rem) { ... }
43+
* 10xl 224rem (3584px) @media (width >= 224rem) { ... }
44+
*/
45+
export const tailwindBreakpoints = {
46+
'sm': { min: 640, max: 767 },
47+
'md': { min: 768, max: 1023 },
48+
'lg': { min: 1024, max: 1279 },
49+
'xl': { min: 1280, max: 1535 },
50+
'2xl': { min: 1536, max: 1791 },
51+
'3xl': { min: 1792, max: 2047 },
52+
'4xl': { min: 2048, max: 2303 },
53+
'5xl': { min: 2304, max: 2559 },
54+
'6xl': { min: 2560, max: 2815 },
55+
'7xl': { min: 2816, max: 3071 },
56+
'8xl': { min: 3072, max: 3327 },
57+
'9xl': { min: 3328, max: 3583 },
58+
'10xl': { min: 3584, max: Infinity },
59+
}
60+
61+
/**
62+
* Common screen resolution breakpoints.
63+
* Mainly for reference or if you want to target specific screen resolutions.
64+
*
65+
* - 720p HD 1280×720
66+
* - 1080p FHD 1920×1080
67+
* - 2K QHD 2560×1440
68+
* - 4K UHD 3840×2160
69+
* - 5K 5120×2880
70+
* - 8K UHD 7680×4320
71+
*
72+
* @see {@link https://en.wikipedia.org/wiki/Display_resolution#Common_display_resolutions}
73+
*/
74+
export const resolutionBreakpoints = {
75+
'720p': { min: 0, max: 1280 },
76+
'1080p': { min: 1281, max: 1920 },
77+
'2k': { min: 1921, max: 2560 },
78+
'4k': { min: 2561, max: 3840 },
79+
'5k': { min: 3841, max: 7680 },
80+
'8k': { min: 7681, max: Infinity },
81+
}
82+
83+
/**
84+
* Achieve responsive sizes based on screen width breakpoints.
85+
* @see {@link https://tailwindcss.com/docs/responsive-design#overview}
86+
*/
87+
export function mapForBreakpoints<
88+
B extends Record<string, { min: number, max: number }> = typeof tailwindBreakpoints,
89+
>(
90+
basedOn: number,
91+
sizes: { [key in keyof B]?: number } | number,
92+
options?: { breakpoints: B },
93+
) {
94+
if (typeof sizes === 'number') {
95+
return sizes
96+
}
97+
98+
const breakpoints = options?.breakpoints ?? tailwindBreakpoints
99+
100+
const matched = Object.entries(breakpoints).find(([, b]) => {
101+
return basedOn >= b.min && basedOn <= b.max
102+
})
103+
104+
if (matched) {
105+
const size = sizes[matched[0]]
106+
if (size) {
107+
return size
108+
}
109+
}
110+
111+
// Fallback: find nearest-least smallest breakpoint
112+
const sortedSizes = Object.entries(sizes)
113+
.map(([key, value]) => ({ key, value, min: breakpoints[key as keyof typeof breakpoints]?.min ?? 0 }))
114+
.sort((a, b) => b.min - a.min) // Sort descending by min width
115+
116+
const fallback = sortedSizes.find(s => s.min <= basedOn)
117+
118+
return fallback?.value ?? Object.values(sizes)?.[0] ?? 0
119+
}
120+
121+
/**
122+
* Calculate width based on options similar to how Web CSS does it.
123+
*
124+
* @param bounds
125+
* @param sizeOptions
126+
* @returns width in pixels
127+
*/
128+
export function widthFrom(bounds: Rectangle, sizeOptions: Size & { min?: Size, max?: Size }) {
129+
const val = evaluateSize(bounds.width, sizeOptions)
130+
const min = sizeOptions.min ? evaluateSize(bounds.width, sizeOptions.min) : undefined
131+
const max = sizeOptions.max ? evaluateSize(bounds.width, sizeOptions.max) : undefined
132+
133+
if (min && val < min) {
134+
return min
135+
}
136+
137+
if (max && val > max) {
138+
return max
139+
}
140+
141+
return val
142+
}
143+
144+
/**
145+
* Calculate height based on options similar to how Web CSS does it.
146+
*
147+
* @param bounds
148+
* @param sizeOptions
149+
* @returns height in pixels
150+
*/
151+
export function heightFrom(bounds: Rectangle, sizeOptions: Size & { min?: Size, max?: Size }) {
152+
const val = evaluateSize(bounds.height, sizeOptions)
153+
const min = sizeOptions.min ? evaluateSize(bounds.height, sizeOptions.min) : undefined
154+
const max = sizeOptions.max ? evaluateSize(bounds.height, sizeOptions.max) : undefined
155+
156+
if (min && val < min) {
157+
return min
158+
}
159+
160+
if (max && val > max) {
161+
return max
162+
}
163+
164+
return val
165+
}

apps/stage-tamagotchi/src/main/windows/shared/window.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,17 @@ export function transparentWindowConfig(): BrowserWindowConstructorOptions {
2323
hasShadow: false,
2424
}
2525
}
26+
27+
export function blurryWindowConfig(): BrowserWindowConstructorOptions {
28+
return {
29+
vibrancy: 'under-window',
30+
backgroundMaterial: 'acrylic',
31+
}
32+
}
33+
34+
export function spotlightLikeWindowConfig(): BrowserWindowConstructorOptions {
35+
return {
36+
...blurryWindowConfig(),
37+
titleBarStyle: isMacOS ? 'hidden' : undefined,
38+
}
39+
}

0 commit comments

Comments
 (0)