Skip to content

Commit bb986cf

Browse files
committed
bubble
1 parent 7f3920e commit bb986cf

File tree

6 files changed

+248
-55
lines changed

6 files changed

+248
-55
lines changed

uipanel/Bubble.swift

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import SwiftUI
2+
3+
// ___________
4+
// / \ c = 2*r
5+
// | |
6+
// | |
7+
// \ _______ / h = 2*height + 1.5*rowGap - shadowRadius
8+
// \/ \/ r
9+
// | | +
10+
// | (x,y) | height - 2*r
11+
// | | +
12+
// \_______/ r
13+
// s + width + s = w
14+
//
15+
// s = 0.4*width
16+
17+
let sideRatio = 0.4
18+
let shadowRadius: CGFloat = 2
19+
20+
enum BubblePosition {
21+
case left
22+
case middle
23+
case right
24+
}
25+
26+
struct BubbleShape: Shape {
27+
let s: CGFloat
28+
let position: BubblePosition
29+
30+
func path(in rect: CGRect) -> Path {
31+
var path = Path()
32+
let w = rect.width
33+
let h = rect.height
34+
let r = keyCornerRadius
35+
let c = r * 2
36+
let left = position == .left ? 0 : (position == .middle ? s : 2 * s)
37+
let right = position == .left ? (w - 2 * s) : (position == .middle ? (w - s) : w)
38+
39+
path.move(to: CGPoint(x: 0, y: c))
40+
// top left corner
41+
path.addArc(
42+
center: CGPoint(x: c, y: c),
43+
radius: c,
44+
startAngle: .degrees(180),
45+
endAngle: .degrees(270),
46+
clockwise: false)
47+
// top
48+
path.addLine(to: CGPoint(x: w - c, y: 0))
49+
// top right corner
50+
path.addArc(
51+
center: CGPoint(x: w - c, y: c),
52+
radius: c,
53+
startAngle: .degrees(270),
54+
endAngle: .degrees(360),
55+
clockwise: false)
56+
// upper right
57+
path.addLine(to: CGPoint(x: w, y: h * 0.4))
58+
// middle right
59+
path.addCurve(
60+
to: CGPoint(x: right, y: h * 0.65),
61+
control1: CGPoint(x: w, y: h * 0.55),
62+
control2: CGPoint(x: right, y: h * 0.5))
63+
// lower right
64+
path.addLine(to: CGPoint(x: right, y: h - r))
65+
// bottom right corner
66+
path.addArc(
67+
center: CGPoint(x: right - r, y: h - r),
68+
radius: r,
69+
startAngle: .degrees(0),
70+
endAngle: .degrees(90),
71+
clockwise: false)
72+
// bottom
73+
path.addLine(to: CGPoint(x: left + r, y: h))
74+
// bottom left corner
75+
path.addArc(
76+
center: CGPoint(x: left + r, y: h - r),
77+
radius: r,
78+
startAngle: .degrees(90),
79+
endAngle: .degrees(180),
80+
clockwise: false)
81+
// lower left
82+
path.addLine(to: CGPoint(x: left, y: h * 0.65))
83+
// middle left
84+
path.addCurve(
85+
to: CGPoint(x: 0, y: h * 0.4),
86+
control1: CGPoint(x: left, y: h * 0.5),
87+
control2: CGPoint(x: 0, y: h * 0.55))
88+
// upper left
89+
path.closeSubpath()
90+
91+
return path
92+
}
93+
}
94+
95+
struct BubbleView: View {
96+
let x: CGFloat
97+
let y: CGFloat
98+
let width: CGFloat
99+
let height: CGFloat
100+
let keyboardWidth: CGFloat
101+
let background: Color
102+
let shadow: Color
103+
let label: String?
104+
105+
var body: some View {
106+
let h = 2 * height + 1.5 * rowGap - shadowRadius
107+
let s = sideRatio * width
108+
let position: BubblePosition =
109+
x - width / 2 - s < 0 ? .left : (x + width / 2 + s > keyboardWidth ? .right : .middle)
110+
let offsetX = position == .left ? s : (position == .middle ? 0 : -s)
111+
BubbleShape(s: sideRatio * width, position: position)
112+
.fill(background)
113+
.shadow(color: shadow, radius: shadowRadius)
114+
.frame(width: (1 + 2 * sideRatio) * width, height: h)
115+
.overlay(
116+
Text(label ?? "").font(.system(size: h * 0.4).weight(.light))
117+
.offset(y: -h / 4)
118+
)
119+
.position(x: x + offsetX, y: y - (h - height) / 2)
120+
}
121+
}

uipanel/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ add_library(KeyboardUI STATIC
33
VirtualKeyboard.swift
44
ContextMenu.swift
55
Key.swift
6+
Bubble.swift
67
KeyModifier.swift
78
Keyboard.swift
89
Candidate.swift

uipanel/Key.swift

Lines changed: 32 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,9 @@ struct KeyView: View {
5050
}
5151
}
5252
),
53-
topRight: subLabel?["topRight"] as? String
53+
topRight: subLabel?["topRight"] as? String,
54+
bubbleLabel: label,
55+
swipeUpLabel: swipeUp?["label"] as? String
5456
)
5557
}
5658
}
@@ -137,39 +139,37 @@ struct GlobeView: View {
137139
let height: CGFloat
138140

139141
var body: some View {
140-
GeometryReader { geometry in
141-
Image(systemName: "globe")
142-
.resizable()
143-
.aspectRatio(contentMode: .fit)
144-
.frame(height: height * 0.45)
145-
.keyProperties(
146-
x: x, y: y, width: width, height: height,
147-
background: getNormalBackground(colorScheme),
148-
pressedBackground: getFunctionBackground(colorScheme),
149-
foreground: getNormalForeground(colorScheme),
150-
shadow: getShadow(colorScheme),
151-
action: GestureAction(
152-
onTap: {
153-
virtualKeyboardView.resetLayerIfNotLocked()
154-
client.globe()
155-
},
156-
onLongPress: {
157-
let items = virtualKeyboardView.viewModel.inputMethods.map { inputMethod in
158-
MenuItem(
159-
text: inputMethod.displayName,
160-
action: {
161-
virtualKeyboardView.resetLayerIfNotLocked()
162-
client.setCurrentInputMethod(inputMethod.name)
163-
})
164-
}
165-
if !items.isEmpty {
166-
let frame = geometry.frame(in: .global)
167-
virtualKeyboardView.showContextMenu(frame, items)
168-
}
142+
Image(systemName: "globe")
143+
.resizable()
144+
.aspectRatio(contentMode: .fit)
145+
.frame(height: height * 0.45)
146+
.keyProperties(
147+
x: x, y: y, width: width, height: height,
148+
background: getNormalBackground(colorScheme),
149+
pressedBackground: getFunctionBackground(colorScheme),
150+
foreground: getNormalForeground(colorScheme),
151+
shadow: getShadow(colorScheme),
152+
action: GestureAction(
153+
onTap: {
154+
virtualKeyboardView.resetLayerIfNotLocked()
155+
client.globe()
156+
},
157+
onLongPress: {
158+
let items = virtualKeyboardView.viewModel.inputMethods.map { inputMethod in
159+
MenuItem(
160+
text: inputMethod.displayName,
161+
action: {
162+
virtualKeyboardView.resetLayerIfNotLocked()
163+
client.setCurrentInputMethod(inputMethod.name)
164+
})
169165
}
170-
)
166+
if !items.isEmpty {
167+
let frame = CGRect(x: x, y: y + barHeight, width: width, height: height)
168+
virtualKeyboardView.showContextMenu(frame, items)
169+
}
170+
}
171171
)
172-
}
172+
)
173173
}
174174
}
175175

uipanel/KeyModifier.swift

Lines changed: 50 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,17 @@ enum SwipeDirection {
1212
case up, down, left, right
1313
}
1414

15+
private func getSwipeDirection(_ dx: CGFloat, _ dy: CGFloat) -> SwipeDirection {
16+
if abs(dx) > abs(dy) {
17+
return dx > 0 ? .right : .left
18+
}
19+
return dy > 0 ? .down : .up
20+
}
21+
22+
private func clearBubble() {
23+
virtualKeyboardView.setBubble(0, 0, 0, 0, .clear, .clear, nil)
24+
}
25+
1526
struct KeyModifier: ViewModifier {
1627
let threshold: CGFloat = 30
1728
let stepSize: CGFloat = 15
@@ -35,6 +46,8 @@ struct KeyModifier: ViewModifier {
3546
let action: GestureAction
3647
let pressedView: (any View)?
3748
let topRight: String?
49+
let bubbleLabel: String?
50+
let swipeUpLabel: String?
3851

3952
func body(content: Content) -> some View {
4053
VStack {
@@ -59,51 +72,68 @@ struct KeyModifier: ViewModifier {
5972
.gesture(
6073
DragGesture(minimumDistance: 0)
6174
.onChanged { value in
75+
let bubbleX = x + width / 2
76+
let bubbleY = y + height / 2
77+
let bubbleWidth = width - columnGap
78+
let bubbleHeight = height - rowGap
79+
6280
if startLocation == nil {
6381
startLocation = value.startLocation
6482
lastLocation = value.startLocation.x
6583
isPressed = true
6684
didTriggerLongPress = false
6785
didMoveFarEnough = false
86+
virtualKeyboardView.setBubble(
87+
bubbleX, bubbleY, bubbleWidth, bubbleHeight, background, shadow, bubbleLabel)
6888

6989
// Schedule long press that can be interrupted by move.
7090
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
7191
if isPressed && !didTriggerLongPress && !didMoveFarEnough {
7292
didTriggerLongPress = true
93+
clearBubble()
7394
action.onLongPress?()
7495
}
7596
}
7697
} else {
7798
let dx = value.location.x - (startLocation?.x ?? 0)
7899
let dy = value.location.y - (startLocation?.y ?? 0)
79100

80-
if !didTriggerLongPress && !didMoveFarEnough {
81-
if abs(dx) > threshold || abs(dy) > threshold {
101+
if !didTriggerLongPress {
102+
if !didMoveFarEnough && (abs(dx) > threshold || abs(dy) > threshold) {
82103
didMoveFarEnough = true
83104
}
105+
if getSwipeDirection(dx, dy) == .up {
106+
virtualKeyboardView.setBubble(
107+
bubbleX, bubbleY, bubbleWidth, bubbleHeight, background, shadow, swipeUpLabel)
108+
} else {
109+
clearBubble()
110+
}
84111
}
85112
// Process slide.
86-
if !slideActivated {
87-
if abs(dx) >= threshold, let start = startLocation {
88-
slideActivated = true
89-
lastLocation = start.x + (dx > 0 ? threshold : -threshold)
113+
if let onSlide = action.onSlide {
114+
if !slideActivated {
115+
if abs(dx) >= threshold, let start = startLocation {
116+
slideActivated = true
117+
lastLocation = start.x + (dx > 0 ? threshold : -threshold)
118+
}
90119
}
91-
}
92-
if slideActivated {
93-
if let start = startLocation, let last = lastLocation {
94-
let totalPast = Int(floor((last - start.x) / stepSize))
95-
let totalNow = Int(floor((value.location.x - start.x) / stepSize))
96-
let delta = totalNow - totalPast
97-
if delta != 0 {
98-
action.onSlide?(delta)
120+
if slideActivated {
121+
if let start = startLocation, let last = lastLocation {
122+
let totalPast = Int(floor((last - start.x) / stepSize))
123+
let totalNow = Int(floor((value.location.x - start.x) / stepSize))
124+
let delta = totalNow - totalPast
125+
if delta != 0 {
126+
onSlide(delta)
127+
}
128+
lastLocation = value.location.x
99129
}
100-
lastLocation = value.location.x
101130
}
102131
}
103132
}
104133
}
105134
.onEnded { value in
106135
isPressed = false
136+
clearBubble()
107137
defer {
108138
startLocation = nil
109139
lastLocation = nil
@@ -122,11 +152,7 @@ struct KeyModifier: ViewModifier {
122152

123153
if didMoveFarEnough {
124154
if !didTriggerLongPress {
125-
if abs(dx) > abs(dy) {
126-
action.onSwipe?(dx > 0 ? .right : .left)
127-
} else {
128-
action.onSwipe?(dy > 0 ? .down : .up)
129-
}
155+
action.onSwipe?(getSwipeDirection(dx, dy))
130156
}
131157
} else {
132158
if !didTriggerLongPress {
@@ -155,14 +181,16 @@ extension View {
155181
x: CGFloat = 0, y: CGFloat = 0,
156182
width: CGFloat, height: CGFloat, background: Color, pressedBackground: Color, foreground: Color,
157183
shadow: Color, action: GestureAction, pressedForeground: Color? = nil,
158-
pressedView: (any View)? = nil, topRight: String? = nil
184+
pressedView: (any View)? = nil, topRight: String? = nil, bubbleLabel: String? = nil,
185+
swipeUpLabel: String? = nil
159186
) -> some View {
160187
self.modifier(
161188
KeyModifier(
162189
x: x, y: y, width: width, height: height, background: background,
163190
pressedBackground: pressedBackground,
164191
foreground: foreground, pressedForeground: pressedForeground ?? foreground,
165-
shadow: shadow, action: action, pressedView: pressedView, topRight: topRight
192+
shadow: shadow, action: action, pressedView: pressedView, topRight: topRight,
193+
bubbleLabel: bubbleLabel, swipeUpLabel: swipeUpLabel
166194
)
167195
)
168196
}

uipanel/Keyboard.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,15 @@ struct KeyboardView: View {
3535
let textIsEmpty: Bool
3636
let enterHighlight: Bool
3737
let hasPreedit: Bool
38+
39+
let bubbleX: CGFloat
40+
let bubbleY: CGFloat
41+
let bubbleWidth: CGFloat
42+
let bubbleHeight: CGFloat
43+
let bubbleBackground: Color
44+
let bubbleShadow: Color
45+
let bubbleLabel: String?
46+
3847
@State private var defaultRows = [[String: Any]]()
3948
@State private var shiftRows = [[String: Any]]()
4049

@@ -45,6 +54,12 @@ struct KeyboardView: View {
4554
ForEach(Array(rows.enumerated()), id: \.offset) { i, row in
4655
renderRow(row, CGFloat(i) * height, width, height)
4756
}
57+
if bubbleLabel != nil {
58+
BubbleView(
59+
x: bubbleX, y: bubbleY, width: bubbleWidth, height: bubbleHeight,
60+
keyboardWidth: width, background: bubbleBackground, shadow: bubbleShadow,
61+
label: bubbleLabel)
62+
}
4863
}
4964
.frame(height: keyboardHeight)
5065
.onAppear {

0 commit comments

Comments
 (0)