|
@@ -86,16 +86,21 @@ struct AppTileView: View {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private var tileContent: some View {
|
|
private var tileContent: some View {
|
|
|
- VStack(spacing: 11) {
|
|
|
|
|
- AppIconView(app: app, size: 64)
|
|
|
|
|
- .shadow(
|
|
|
|
|
- color: .black.opacity(isHovering ? 0.34 : 0.2),
|
|
|
|
|
- radius: isHovering ? 14 : 8,
|
|
|
|
|
- x: 0,
|
|
|
|
|
- y: isHovering ? 10 : 5
|
|
|
|
|
- )
|
|
|
|
|
- .scaleEffect(isPressing ? 0.96 : 1.0)
|
|
|
|
|
- .offset(y: isHovering ? -1 : 0)
|
|
|
|
|
|
|
+ VStack(spacing: 9) {
|
|
|
|
|
+ AppIconView(
|
|
|
|
|
+ app: app,
|
|
|
|
|
+ size: 48,
|
|
|
|
|
+ showAppBackground: false,
|
|
|
|
|
+ iconPaddingFactor: 0.07
|
|
|
|
|
+ )
|
|
|
|
|
+ .shadow(
|
|
|
|
|
+ color: .black.opacity(isHovering ? 0.24 : 0.06),
|
|
|
|
|
+ radius: isHovering ? 12 : 5,
|
|
|
|
|
+ x: 0,
|
|
|
|
|
+ y: isHovering ? 8 : 2
|
|
|
|
|
+ )
|
|
|
|
|
+ .scaleEffect(isPressing ? 0.97 : (isHovering ? 1.02 : 1.0))
|
|
|
|
|
+ .offset(y: isHovering ? -0.5 : 0)
|
|
|
|
|
|
|
|
Text(app.name)
|
|
Text(app.name)
|
|
|
.font(.system(size: 13, weight: .medium))
|
|
.font(.system(size: 13, weight: .medium))
|
|
@@ -103,61 +108,53 @@ struct AppTileView: View {
|
|
|
.lineLimit(1)
|
|
.lineLimit(1)
|
|
|
.truncationMode(.tail)
|
|
.truncationMode(.tail)
|
|
|
.frame(maxWidth: .infinity)
|
|
.frame(maxWidth: .infinity)
|
|
|
- .offset(y: isHovering ? -0.5 : 0)
|
|
|
|
|
|
|
+ .offset(y: isHovering ? -0.25 : 0)
|
|
|
}
|
|
}
|
|
|
- .padding(.horizontal, 7)
|
|
|
|
|
- .padding(.vertical, 10)
|
|
|
|
|
|
|
+ .padding(.horizontal, 10)
|
|
|
|
|
+ .padding(.vertical, 12)
|
|
|
.background(
|
|
.background(
|
|
|
RoundedRectangle(cornerRadius: tileCornerRadius, style: .continuous)
|
|
RoundedRectangle(cornerRadius: tileCornerRadius, style: .continuous)
|
|
|
- .fill(
|
|
|
|
|
- LinearGradient(
|
|
|
|
|
- colors: isHovering
|
|
|
|
|
- ? [Color.white.opacity(0.16), Color.white.opacity(0.06)]
|
|
|
|
|
- : [Color.white.opacity(0.03), Color.white.opacity(0.015)],
|
|
|
|
|
- startPoint: .topLeading,
|
|
|
|
|
- endPoint: .bottomTrailing
|
|
|
|
|
- )
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ .fill(Color.white.opacity(isHovering ? 0.07 : 0.02))
|
|
|
)
|
|
)
|
|
|
.overlay(
|
|
.overlay(
|
|
|
- // Soft spotlight makes hover feel richer without harsh contrast.
|
|
|
|
|
RoundedRectangle(cornerRadius: tileCornerRadius, style: .continuous)
|
|
RoundedRectangle(cornerRadius: tileCornerRadius, style: .continuous)
|
|
|
- .fill(
|
|
|
|
|
- RadialGradient(
|
|
|
|
|
- colors: isHovering
|
|
|
|
|
- ? [Color.white.opacity(0.16), Color.clear]
|
|
|
|
|
- : [Color.clear, Color.clear],
|
|
|
|
|
- center: .top,
|
|
|
|
|
- startRadius: 0,
|
|
|
|
|
- endRadius: 80
|
|
|
|
|
- )
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ .fill(RadialGradient(
|
|
|
|
|
+ colors: [
|
|
|
|
|
+ Color.white.opacity(isHovering ? 0.16 : 0.06),
|
|
|
|
|
+ .clear
|
|
|
|
|
+ ],
|
|
|
|
|
+ center: .top,
|
|
|
|
|
+ startRadius: 12,
|
|
|
|
|
+ endRadius: 100
|
|
|
|
|
+ ))
|
|
|
.allowsHitTesting(false)
|
|
.allowsHitTesting(false)
|
|
|
)
|
|
)
|
|
|
.overlay(
|
|
.overlay(
|
|
|
RoundedRectangle(cornerRadius: tileCornerRadius, style: .continuous)
|
|
RoundedRectangle(cornerRadius: tileCornerRadius, style: .continuous)
|
|
|
.stroke(
|
|
.stroke(
|
|
|
- isHovering ? Color.white.opacity(0.3) : Color.white.opacity(0.06),
|
|
|
|
|
- lineWidth: isHovering ? 1.3 : 1
|
|
|
|
|
|
|
+ isHovering ? Color.white.opacity(0.28) : Color.white.opacity(0.10),
|
|
|
|
|
+ lineWidth: isHovering ? 1.2 : 1
|
|
|
)
|
|
)
|
|
|
)
|
|
)
|
|
|
.overlay(
|
|
.overlay(
|
|
|
RoundedRectangle(cornerRadius: tileCornerRadius, style: .continuous)
|
|
RoundedRectangle(cornerRadius: tileCornerRadius, style: .continuous)
|
|
|
.inset(by: 1)
|
|
.inset(by: 1)
|
|
|
.stroke(
|
|
.stroke(
|
|
|
- isHovering ? Color.white.opacity(0.12) : Color.clear,
|
|
|
|
|
|
|
+ isHovering ? Color.white.opacity(0.16) : Color.white.opacity(0.04),
|
|
|
lineWidth: 1
|
|
lineWidth: 1
|
|
|
)
|
|
)
|
|
|
.allowsHitTesting(false)
|
|
.allowsHitTesting(false)
|
|
|
)
|
|
)
|
|
|
- .scaleEffect(isPressing ? 0.985 : (isHovering ? 1.02 : 1.0))
|
|
|
|
|
- .offset(y: isPressing ? 0 : (isHovering ? -2 : 0))
|
|
|
|
|
|
|
+ .scaleEffect(isPressing ? 0.99 : (isHovering ? 1.02 : 1.0))
|
|
|
|
|
+ .offset(y: isPressing ? 0 : (isHovering ? -1 : 0))
|
|
|
.shadow(
|
|
.shadow(
|
|
|
color: .black.opacity(isHovering ? 0.22 : 0),
|
|
color: .black.opacity(isHovering ? 0.22 : 0),
|
|
|
radius: isHovering ? 18 : 0,
|
|
radius: isHovering ? 18 : 0,
|
|
|
x: 0,
|
|
x: 0,
|
|
|
y: isHovering ? 12 : 0
|
|
y: isHovering ? 12 : 0
|
|
|
)
|
|
)
|
|
|
|
|
+ // Ensure tiles look consistent across the adaptive grid.
|
|
|
|
|
+ .frame(maxWidth: .infinity, minHeight: 94)
|
|
|
.contentShape(RoundedRectangle(cornerRadius: tileCornerRadius, style: .continuous))
|
|
.contentShape(RoundedRectangle(cornerRadius: tileCornerRadius, style: .continuous))
|
|
|
.animation(.spring(response: 0.24, dampingFraction: 0.88), value: isHovering)
|
|
.animation(.spring(response: 0.24, dampingFraction: 0.88), value: isHovering)
|
|
|
.animation(.easeOut(duration: 0.12), value: isPressing)
|
|
.animation(.easeOut(duration: 0.12), value: isPressing)
|
|
@@ -269,6 +266,8 @@ private struct HideMenuIndicatorIfAvailable: ViewModifier {
|
|
|
private struct AppIconView: View {
|
|
private struct AppIconView: View {
|
|
|
let app: LauncherApp
|
|
let app: LauncherApp
|
|
|
let size: CGFloat
|
|
let size: CGFloat
|
|
|
|
|
+ var showAppBackground: Bool = true
|
|
|
|
|
+ var iconPaddingFactor: CGFloat = 0.12
|
|
|
|
|
|
|
|
var body: some View {
|
|
var body: some View {
|
|
|
ZStack {
|
|
ZStack {
|
|
@@ -283,27 +282,31 @@ private struct AppIconView: View {
|
|
|
.font(.system(size: size * 0.34, weight: .regular))
|
|
.font(.system(size: size * 0.34, weight: .regular))
|
|
|
.foregroundStyle(.white.opacity(0.9))
|
|
.foregroundStyle(.white.opacity(0.9))
|
|
|
} else {
|
|
} else {
|
|
|
- RoundedRectangle(cornerRadius: 18, style: .continuous)
|
|
|
|
|
- .fill(LinearGradient(colors: gradientColors, startPoint: .topLeading, endPoint: .bottomTrailing))
|
|
|
|
|
- RoundedRectangle(cornerRadius: 18, style: .continuous)
|
|
|
|
|
- .stroke(Color.white.opacity(0.08), lineWidth: 1)
|
|
|
|
|
|
|
+ if showAppBackground {
|
|
|
|
|
+ RoundedRectangle(cornerRadius: 18, style: .continuous)
|
|
|
|
|
+ .fill(LinearGradient(colors: gradientColors, startPoint: .topLeading, endPoint: .bottomTrailing))
|
|
|
|
|
+ RoundedRectangle(cornerRadius: 18, style: .continuous)
|
|
|
|
|
+ .stroke(Color.white.opacity(0.08), lineWidth: 1)
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
if let webURL = app.webURL {
|
|
if let webURL = app.webURL {
|
|
|
WebSiteFaviconView(
|
|
WebSiteFaviconView(
|
|
|
webURL: webURL,
|
|
webURL: webURL,
|
|
|
size: size,
|
|
size: size,
|
|
|
assetIconName: app.assetIconName,
|
|
assetIconName: app.assetIconName,
|
|
|
- fallbackSymbolName: app.fallbackSymbolName
|
|
|
|
|
|
|
+ fallbackSymbolName: app.fallbackSymbolName,
|
|
|
|
|
+ iconPaddingFactor: iconPaddingFactor
|
|
|
)
|
|
)
|
|
|
} else if let iconImage = NSImage(named: app.assetIconName) {
|
|
} else if let iconImage = NSImage(named: app.assetIconName) {
|
|
|
Image(nsImage: iconImage)
|
|
Image(nsImage: iconImage)
|
|
|
.resizable()
|
|
.resizable()
|
|
|
.scaledToFit()
|
|
.scaledToFit()
|
|
|
- .padding(size * 0.12)
|
|
|
|
|
|
|
+ .padding(size * iconPaddingFactor)
|
|
|
} else {
|
|
} else {
|
|
|
Image(systemName: app.fallbackSymbolName)
|
|
Image(systemName: app.fallbackSymbolName)
|
|
|
.font(.system(size: size * 0.32, weight: .semibold))
|
|
.font(.system(size: size * 0.32, weight: .semibold))
|
|
|
.foregroundStyle(.white)
|
|
.foregroundStyle(.white)
|
|
|
|
|
+ .padding(size * iconPaddingFactor)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|