Skip to content

Instantly share code, notes, and snippets.

@ethanhuang13
Last active July 23, 2025 01:18
Show Gist options
  • Select an option

  • Save ethanhuang13/8587d10689e3735354f975f6a25ef9fa to your computer and use it in GitHub Desktop.

Select an option

Save ethanhuang13/8587d10689e3735354f975f6a25ef9fa to your computer and use it in GitHub Desktop.

Revisions

  1. ethanhuang13 revised this gist Feb 26, 2025. 1 changed file with 299 additions and 271 deletions.
    570 changes: 299 additions & 271 deletions VirtualKeyboard.swift
    Original file line number Diff line number Diff line change
    @@ -8,308 +8,336 @@
    import SwiftUI

    struct VirtualKeyboard: View {
    var body: some View {
    VStack(spacing: .zero) {
    HStack(spacing: .zero) {
    speaker
    keyboard
    .padding(.horizontal, 15)
    speaker
    }
    .padding(.horizontal, 15)
    .padding(.vertical, 15)
    var body: some View {
    VStack(spacing: 16) {
    HStack(spacing: 16) {
    speaker
    keyboard
    speaker
    }

    trackPad
    }
    .padding(.vertical)
    .background(Color(white: 0.6))
    .clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous))
    trackPad
    }
    .padding(.top, 32 + 16)
    .padding(16)
    .background(Color.aluminumAlloy)
    .clipShape(RoundedRectangle(cornerRadius: 32, style: .continuous))
    .preferredColorScheme(.dark)
    }

    private var speaker: some View {
    Color(white: 0.5)
    .frame(width: defaultWidth * 0.6)
    .padding(.vertical)
    }
    @ViewBuilder
    private var speaker: some View {
    Color.speakers
    .frame(width: 32)
    .padding(.vertical)
    }

    private var trackPad: some View {
    HStack { // Touchpad
    Color(white: 0.5)
    .clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous))
    .frame(width: defaultWidth * 8.8, height: defaultHeight * 5)
    }
    @ViewBuilder
    private var trackPad: some View {
    HStack {
    Color.trackpad
    .clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous))
    .frame(width: defaultWidth * 8.8, height: defaultHeight * 5)
    }
    }

    private var keyboard: some View {
    VStack(spacing: defaultWidth / 10) {
    let allKeys: [[KeyCap.Config]] = [
    [.esc,
    .fn(top: "􀆫", bottom: "F1"),
    .fn(top: "􀆭", bottom: "F2"),
    .fn(top: "􀇴", bottom: "F3"),
    .fn(top: "􀊫", bottom: "F4"),
    .fn(top: "􀊰", bottom: "F5"),
    .fn(top: "􀆹", bottom: "F6"),
    .fn(top: "􀊉", bottom: "F7"),
    .fn(top: "􀊇", bottom: "F8"),
    .fn(top: "􀊋", bottom: "F9"),
    .fn(top: "􀊠", bottom: "F10"),
    .fn(top: "􀊤", bottom: "F11"),
    .fn(top: "􀊨", bottom: "F12"),
    .touchId],
    [.topBottom(top: "~", bottom: "."),
    .grid(["!", "1", "", ""]),
    .grid(["@", "2", "", ""]),
    .grid(["#", "3", "ˇ", ""]),
    .grid(["$", "4", "ˋ", ""]),
    .grid(["%", "5", "", ""]),
    .grid(["^", "6", "ˊ", ""]),
    .grid(["&", "7", "˙", ""]),
    .grid(["*", "8", "", ""]),
    .grid(["(", "9", "", ""]),
    .grid([")", "0", "", ""]),
    .grid(["_", "-", "", ""]),
    .topBottom(top: "+", bottom: "="),
    .backspace],
    [.tab,
    .grid(["", "Q", "", ""]),
    .grid(["", "W", "", ""]),
    .grid(["", "E", "", ""]),
    .grid(["", "R", "", ""]),
    .grid(["", "T", "", ""]),
    .grid(["", "Y", "", ""]),
    .grid(["", "U", "", ""]),
    .grid(["", "I", "", ""]),
    .grid(["", "O", "", ""]),
    .grid(["", "P", "", ""]),
    .grid(["", "{", "", "["]),
    .grid(["", "}", "", "]"]),
    .grid(["", "|", "", "\\"])],
    [.capsLock,
    .grid(["", "A", "", ""]),
    .grid(["", "S", "", ""]),
    .grid(["", "D", "", ""]),
    .grid(["", "F", "", ""]),
    .grid(["", "G", "", ""]),
    .grid(["", "H", "", ""]),
    .grid(["", "J", "", ""]),
    .grid(["", "K", "", ""]),
    .grid(["", "L", "", ""]),
    .grid(["", ":", "", ";"]),
    .topBottom(top: "\"", bottom: "'"),
    .return],
    [.leftShift,
    .grid(["", "Z", "", ""]),
    .grid(["", "X", "", ""]),
    .grid(["", "C", "", ""]),
    .grid(["", "V", "", ""]),
    .grid(["", "B", "", ""]),
    .grid(["", "N", "", ""]),
    .grid(["", "M", "", ""]),
    .grid(["", "<", "", ","]),
    .grid(["", ">", "", "."]),
    .grid(["?", "", "", "/"]),
    .rightShift],
    [.globe,
    .control,
    .leftOption,
    .leftCommand,
    .space,
    .rightCommand,
    .rightOption,
    .directions]
    ]
    ForEach(0 ..< allKeys.count) { index in
    let keys = allKeys[index]
    HStack {
    ForEach(keys.map { AnyIdentifiable($0) }) {
    KeyCap($0.value)
    }
    }
    }
    @ViewBuilder
    private var keyboard: some View {
    VStack(spacing: defaultWidth / 10) {
    let allKeys: [[KeyCap.Config]] = [
    [
    .esc,
    .fn(top: "􀆫", bottom: "F1"),
    .fn(top: "􀆭", bottom: "F2"),
    .fn(top: "􀇴", bottom: "F3"),
    .fn(top: "􀊫", bottom: "F4"),
    .fn(top: "􀊰", bottom: "F5"),
    .fn(top: "􀆹", bottom: "F6"),
    .fn(top: "􀊉", bottom: "F7"),
    .fn(top: "􀊇", bottom: "F8"),
    .fn(top: "􀊋", bottom: "F9"),
    .fn(top: "􀊠", bottom: "F10"),
    .fn(top: "􀊤", bottom: "F11"),
    .fn(top: "􀊨", bottom: "F12"),
    .touchID,
    ],
    [
    .topBottom(top: "~", bottom: "."),
    .grid(["!", "1", "", ""]),
    .grid(["@", "2", "", ""]),
    .grid(["#", "3", "ˇ", ""]),
    .grid(["$", "4", "ˋ", ""]),
    .grid(["%", "5", "", ""]),
    .grid(["^", "6", "ˊ", ""]),
    .grid(["&", "7", "˙", ""]),
    .grid(["*", "8", "", ""]),
    .grid(["(", "9", "", ""]),
    .grid([")", "0", "", ""]),
    .grid(["_", "-", "", ""]),
    .topBottom(top: "+", bottom: "="),
    .backspace,
    ],
    [
    .tab,
    .grid(["", "Q", "", ""]),
    .grid(["", "W", "", ""]),
    .grid(["", "E", "", ""]),
    .grid(["", "R", "", ""]),
    .grid(["", "T", "", ""]),
    .grid(["", "Y", "", ""]),
    .grid(["", "U", "", ""]),
    .grid(["", "I", "", ""]),
    .grid(["", "O", "", ""]),
    .grid(["", "P", "", ""]),
    .grid(["", "{", "", "["]),
    .grid(["", "}", "", "]"]),
    .grid(["", "|", "", "\\"]),
    ],
    [
    .capsLock,
    .grid(["", "A", "", ""]),
    .grid(["", "S", "", ""]),
    .grid(["", "D", "", ""]),
    .grid(["", "F", "", ""]),
    .grid(["", "G", "", ""]),
    .grid(["", "H", "", ""]),
    .grid(["", "J", "", ""]),
    .grid(["", "K", "", ""]),
    .grid(["", "L", "", ""]),
    .grid(["", ":", "", ";"]),
    .topBottom(top: "\"", bottom: "'"),
    .return,
    ],
    [
    .leftShift,
    .grid(["", "Z", "", ""]),
    .grid(["", "X", "", ""]),
    .grid(["", "C", "", ""]),
    .grid(["", "V", "", ""]),
    .grid(["", "B", "", ""]),
    .grid(["", "N", "", ""]),
    .grid(["", "M", "", ""]),
    .grid(["", "<", "", ","]),
    .grid(["", ">", "", "."]),
    .grid(["?", "", "", "/"]),
    .rightShift,
    ],
    [
    .globe,
    .control,
    .leftOption,
    .leftCommand,
    .space,
    .rightCommand,
    .rightOption,
    .directions,
    ],
    ]
    ForEach(0..<allKeys.count, id: \.self) { index in
    let keys = allKeys[index]
    HStack {
    ForEach(keys, id: \.self) { key in
    KeyCap(key)
    }
    }
    .padding(10)
    .background(Color(white: 0.5))
    .clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous))
    }
    }
    .padding(8)
    .background(Color(white: 0.5))
    .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
    }
    }

    private let fnHeight: CGFloat = 25
    private let defaultWidth: CGFloat = 50
    private let defaultHeight: CGFloat = 50
    private let fnHeight: CGFloat = 24
    private let defaultWidth: CGFloat = 48
    private let defaultHeight: CGFloat = 48

    struct AnyIdentifiable<T>: Identifiable {
    let id = UUID()
    let value: T
    init(_ value: T) {
    self.value = value
    }
    extension Color {
    static let aluminumAlloy = Color(white: 0.6)
    static let speakers = Color(white: 0.5)
    static let trackpad = Color(white: 0.5)
    }

    struct KeyCap: View {
    enum Config {
    case esc
    case fn(top: String, bottom: String)
    case touchId
    case topBottom(top: String, bottom: String)
    case grid([String])
    case backspace
    case tab
    case capsLock, `return`
    case leftShift, rightShift
    case globe, control, leftOption, leftCommand, space, rightCommand, rightOption
    case directions
    }
    enum Config: Hashable {
    case esc
    case fn(top: String, bottom: String)
    case touchID
    case topBottom(top: String, bottom: String)
    case grid([String])
    case backspace
    case tab
    case capsLock, `return`
    case leftShift, rightShift
    case globe, control, leftOption, leftCommand, space, rightCommand,
    rightOption
    case directions
    }

    init(_ config: Config) {
    self.config = config
    }
    init(_ config: Config) {
    self.config = config
    }

    let config: Config
    let config: Config

    var body: some View {
    if case Config.directions = config {
    key
    } else {
    key
    .padding(5)
    .background(Color.black)
    .clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
    }
    var body: some View {
    if case Config.directions = config {
    key
    } else {
    key
    .padding(5)
    .background(.background)
    .clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
    }
    }

    private struct Grid: View {
    let keys: [String]
    private struct Grid: View {
    let keys: [String]

    var body: some View {
    let columns = [GridItem(.fixed(defaultWidth / 2), spacing: 5),
    GridItem(.fixed(defaultWidth / 2), spacing: 5)]
    LazyVGrid(columns: columns, spacing: 0, content: {
    ForEach(keys.map { AnyIdentifiable($0) }) {
    Text($0.value)
    .padding(0)
    }
    })
    }
    var body: some View {
    let columns = [
    GridItem(.fixed(defaultWidth / 2), spacing: 5),
    GridItem(.fixed(defaultWidth / 2), spacing: 5),
    ]
    LazyVGrid(
    columns: columns, spacing: 0,
    content: {
    ForEach(keys, id: \.self) { key in
    Text(key)
    }
    })
    }
    }

    private struct TopBottom: View {
    init(top: String, bottom: String, alignment: HorizontalAlignment = .center) {
    self.top = top
    self.bottom = bottom
    self.alignment = alignment
    }
    private struct TopBottom: View {
    init(top: String, bottom: String, alignment: HorizontalAlignment = .center)
    {
    self.top = top
    self.bottom = bottom
    self.alignment = alignment
    }

    let top: String
    let bottom: String
    let alignment: HorizontalAlignment
    let top: String
    let bottom: String
    let alignment: HorizontalAlignment

    var body: some View {
    VStack(alignment: alignment, spacing: 5) {
    Text(top)
    .padding(.horizontal, 3)
    Text(bottom)
    .padding(.horizontal, 3)
    }
    }
    var body: some View {
    VStack(alignment: alignment, spacing: 5) {
    Text(top)
    .padding(.horizontal, 3)
    Text(bottom)
    .padding(.horizontal, 3)
    }
    }
    }

    private struct Arrow: View {
    let key: String
    private struct Arrow: View {
    let key: String

    var body: some View {
    Text(key)
    .font(.caption)
    .frame(width: defaultWidth * 1.15, height: defaultHeight / 1.7)
    .background(Color.black)
    .clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
    }
    var body: some View {
    Text(key)
    .font(.caption)
    .frame(width: defaultWidth * 1.15, height: defaultHeight / 1.7)
    .background(.background)
    .clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
    }
    }

    @ViewBuilder
    var key: some View {
    switch config {
    case .esc:
    HStack {
    Text("esc")
    .frame(width: defaultWidth, height: fnHeight, alignment: .leading)
    }
    case .fn(let top, let bottom):
    VStack {
    Text(top)
    Text(bottom)
    .font(.caption2)
    }
    .frame(width: defaultWidth * 1.083, height: fnHeight)
    case .touchId:
    Text("")
    .frame(width: defaultWidth / 2, height: fnHeight)
    case .topBottom(let top, let bottom):
    TopBottom(top: top, bottom: bottom)
    .frame(width: defaultWidth, height: defaultHeight)
    .font(.title2)
    .frame(width: defaultWidth, height: defaultHeight)
    case .grid(let keys):
    Grid(keys: keys)
    .font(.title2)
    .frame(width: defaultWidth, height: defaultHeight)
    case .backspace:
    TopBottom(top: "", bottom: "􀆛")
    .frame(width: defaultWidth * 1.5, height: defaultHeight, alignment: .trailing)
    case .tab:
    TopBottom(top: "", bottom: "􀅆")
    .frame(width: defaultWidth * 1.5, height: defaultHeight, alignment: .leading)
    case .capsLock:
    TopBottom(top: "", bottom: "中/英", alignment: .leading)
    .frame(width: defaultWidth * 2, height: defaultHeight, alignment: .leading)
    case .return:
    TopBottom(top: "", bottom: "􀅇")
    .frame(width: defaultWidth * 1.9, height: defaultHeight, alignment: .trailing)
    case .leftShift:
    TopBottom(top: "", bottom: "􀆝")
    .frame(width: defaultWidth * 2.6, height: defaultHeight, alignment: .leading)
    case .rightShift:
    TopBottom(top: "", bottom: "􀆝")
    .frame(width: defaultWidth * 2.6, height: defaultHeight, alignment: .trailing)
    case .globe:
    Grid(keys: ["", "fn", "􀆪", ""])
    .font(.title3)
    .frame(width: defaultWidth, height: defaultHeight)
    case .control:
    TopBottom(top: "􀆍", bottom: "control", alignment: .trailing)
    .frame(width: defaultWidth, height: defaultHeight, alignment: .trailing)
    case .leftOption:
    TopBottom(top: "􀆕", bottom: "option", alignment: .trailing)
    .frame(width: defaultWidth, height: defaultHeight, alignment: .trailing)
    case .leftCommand:
    TopBottom(top: "􀆔", bottom: "command", alignment: .trailing)
    .frame(width: defaultWidth * 1.35, height: defaultHeight, alignment: .trailing)
    case .space:
    Color.clear
    .frame(width: defaultWidth * 6.4, height: defaultHeight)
    case .rightCommand:
    TopBottom(top: "􀆔", bottom: "command", alignment: .trailing)
    .frame(width: defaultWidth * 1.35, height: defaultHeight, alignment: .leading)
    case .rightOption:
    TopBottom(top: "􀆕", bottom: "option", alignment: .trailing)
    .frame(width: defaultWidth, height: defaultHeight, alignment: .leading)
    case .directions:
    VStack(spacing: 0) {
    HStack {
    Arrow(key: "􀄤")
    }
    HStack(spacing: 8) {
    Arrow(key: "􀄦")
    Arrow(key: "􀄥")
    Arrow(key: "􀄧")
    }
    }
    @ViewBuilder
    var key: some View {
    switch config {
    case .esc:
    HStack {
    Text("esc")
    .frame(width: defaultWidth, height: fnHeight, alignment: .leading)
    }
    case .fn(let top, let bottom):
    VStack {
    Text(top)
    Text(bottom)
    .font(.caption2)
    }
    .frame(width: defaultWidth * 1.083, height: fnHeight)
    case .touchID:
    Text("")
    .frame(width: defaultWidth / 2, height: fnHeight)
    case .topBottom(let top, let bottom):
    TopBottom(top: top, bottom: bottom)
    .frame(width: defaultWidth, height: defaultHeight)
    .font(.title2)
    .frame(width: defaultWidth, height: defaultHeight)
    case .grid(let keys):
    Grid(keys: keys)
    .font(.title2)
    .frame(width: defaultWidth, height: defaultHeight)
    case .backspace:
    TopBottom(top: "", bottom: "􀆛")
    .frame(
    width: defaultWidth * 1.5, height: defaultHeight, alignment: .trailing
    )
    case .tab:
    TopBottom(top: "", bottom: "􀅆")
    .frame(
    width: defaultWidth * 1.5, height: defaultHeight, alignment: .leading)
    case .capsLock:
    TopBottom(top: "", bottom: "中/英", alignment: .leading)
    .frame(
    width: defaultWidth * 2, height: defaultHeight, alignment: .leading)
    case .return:
    TopBottom(top: "", bottom: "􀅇")
    .frame(
    width: defaultWidth * 1.9, height: defaultHeight, alignment: .trailing
    )
    case .leftShift:
    TopBottom(top: "", bottom: "􀆝")
    .frame(
    width: defaultWidth * 2.6, height: defaultHeight, alignment: .leading)
    case .rightShift:
    TopBottom(top: "", bottom: "􀆝")
    .frame(
    width: defaultWidth * 2.6, height: defaultHeight, alignment: .trailing
    )
    case .globe:
    Grid(keys: ["", "fn", "􀆪", ""])
    .font(.title3)
    .frame(width: defaultWidth, height: defaultHeight)
    case .control:
    TopBottom(top: "􀆍", bottom: "control", alignment: .trailing)
    .frame(width: defaultWidth, height: defaultHeight, alignment: .trailing)
    case .leftOption:
    TopBottom(top: "􀆕", bottom: "option", alignment: .trailing)
    .frame(width: defaultWidth, height: defaultHeight, alignment: .trailing)
    case .leftCommand:
    TopBottom(top: "􀆔", bottom: "command", alignment: .trailing)
    .frame(
    width: defaultWidth * 1.35, height: defaultHeight,
    alignment: .trailing)
    case .space:
    Color.clear
    .frame(width: defaultWidth * 6.4, height: defaultHeight)
    case .rightCommand:
    TopBottom(top: "􀆔", bottom: "command", alignment: .trailing)
    .frame(
    width: defaultWidth * 1.35, height: defaultHeight, alignment: .leading
    )
    case .rightOption:
    TopBottom(top: "􀆕", bottom: "option", alignment: .trailing)
    .frame(width: defaultWidth, height: defaultHeight, alignment: .leading)
    case .directions:
    VStack(spacing: 0) {
    HStack {
    Arrow(key: "􀄤")
    }
    HStack(spacing: 8) {
    Arrow(key: "􀄦")
    Arrow(key: "􀄥")
    Arrow(key: "􀄧")
    }
    }
    }
    }
    }

    struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
    VirtualKeyboard()
    }
    #Preview {
    VirtualKeyboard()
    }
  2. ethanhuang13 revised this gist Jan 15, 2021. 1 changed file with 0 additions and 21 deletions.
    21 changes: 0 additions & 21 deletions LICENSE
    Original file line number Diff line number Diff line change
    @@ -1,21 +0,0 @@
    MIT License

    Copyright (c) 2021 Ethan Huang

    Permission is hereby granted, free of charge, to any person obtaining a copy
    of this software and associated documentation files (the "Software"), to deal
    in the Software without restriction, including without limitation the rights
    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    copies of the Software, and to permit persons to whom the Software is
    furnished to do so, subject to the following conditions:

    The above copyright notice and this permission notice shall be included in all
    copies or substantial portions of the Software.

    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    SOFTWARE.
  3. ethanhuang13 revised this gist Jan 15, 2021. 1 changed file with 6 additions and 6 deletions.
    12 changes: 6 additions & 6 deletions VirtualKeyboard.swift
    Original file line number Diff line number Diff line change
    @@ -36,7 +36,7 @@ struct VirtualKeyboard: View {
    HStack { // Touchpad
    Color(white: 0.5)
    .clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous))
    .frame(width: defaultWidth * 9, height: defaultHeight * 5)
    .frame(width: defaultWidth * 8.8, height: defaultHeight * 5)
    }
    }

    @@ -220,7 +220,7 @@ struct KeyCap: View {
    var body: some View {
    Text(key)
    .font(.caption)
    .frame(width: defaultWidth, height: defaultHeight / 1.7)
    .frame(width: defaultWidth * 1.15, height: defaultHeight / 1.7)
    .background(Color.black)
    .clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
    }
    @@ -283,13 +283,13 @@ struct KeyCap: View {
    .frame(width: defaultWidth, height: defaultHeight, alignment: .trailing)
    case .leftCommand:
    TopBottom(top: "􀆔", bottom: "command", alignment: .trailing)
    .frame(width: defaultWidth * 1.4, height: defaultHeight, alignment: .trailing)
    .frame(width: defaultWidth * 1.35, height: defaultHeight, alignment: .trailing)
    case .space:
    Color.clear
    .frame(width: defaultWidth * 7, height: defaultHeight)
    .frame(width: defaultWidth * 6.4, height: defaultHeight)
    case .rightCommand:
    TopBottom(top: "􀆔", bottom: "command", alignment: .trailing)
    .frame(width: defaultWidth * 1.4, height: defaultHeight, alignment: .leading)
    .frame(width: defaultWidth * 1.35, height: defaultHeight, alignment: .leading)
    case .rightOption:
    TopBottom(top: "􀆕", bottom: "option", alignment: .trailing)
    .frame(width: defaultWidth, height: defaultHeight, alignment: .leading)
    @@ -298,7 +298,7 @@ struct KeyCap: View {
    HStack {
    Arrow(key: "􀄤")
    }
    HStack(spacing: 5) {
    HStack(spacing: 8) {
    Arrow(key: "􀄦")
    Arrow(key: "􀄥")
    Arrow(key: "􀄧")
  4. ethanhuang13 revised this gist Jan 15, 2021. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion VirtualKeyboard.swift
    Original file line number Diff line number Diff line change
    @@ -36,7 +36,7 @@ struct VirtualKeyboard: View {
    HStack { // Touchpad
    Color(white: 0.5)
    .clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous))
    .frame(width: defaultWidth * 6.5, height: defaultHeight * 4)
    .frame(width: defaultWidth * 9, height: defaultHeight * 5)
    }
    }

  5. ethanhuang13 revised this gist Jan 15, 2021. 1 changed file with 21 additions and 0 deletions.
    21 changes: 21 additions & 0 deletions LICENSE
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,21 @@
    MIT License

    Copyright (c) 2021 Ethan Huang

    Permission is hereby granted, free of charge, to any person obtaining a copy
    of this software and associated documentation files (the "Software"), to deal
    in the Software without restriction, including without limitation the rights
    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    copies of the Software, and to permit persons to whom the Software is
    furnished to do so, subject to the following conditions:

    The above copyright notice and this permission notice shall be included in all
    copies or substantial portions of the Software.

    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    SOFTWARE.
  6. ethanhuang13 created this gist Jan 15, 2021.
    315 changes: 315 additions & 0 deletions VirtualKeyboard.swift
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,315 @@
    //
    // VirtualKeyboard.swift
    // MacBook Air M1 Zhuyin Keyboard
    // Written with SwiftUI ~=300 LOC, < 4hrs
    // Created by Ethan Huang on 2021/1/13.
    // Twitter: @ethanhuang13

    import SwiftUI

    struct VirtualKeyboard: View {
    var body: some View {
    VStack(spacing: .zero) {
    HStack(spacing: .zero) {
    speaker
    keyboard
    .padding(.horizontal, 15)
    speaker
    }
    .padding(.horizontal, 15)
    .padding(.vertical, 15)

    trackPad
    }
    .padding(.vertical)
    .background(Color(white: 0.6))
    .clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous))
    }

    private var speaker: some View {
    Color(white: 0.5)
    .frame(width: defaultWidth * 0.6)
    .padding(.vertical)
    }

    private var trackPad: some View {
    HStack { // Touchpad
    Color(white: 0.5)
    .clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous))
    .frame(width: defaultWidth * 6.5, height: defaultHeight * 4)
    }
    }

    private var keyboard: some View {
    VStack(spacing: defaultWidth / 10) {
    let allKeys: [[KeyCap.Config]] = [
    [.esc,
    .fn(top: "􀆫", bottom: "F1"),
    .fn(top: "􀆭", bottom: "F2"),
    .fn(top: "􀇴", bottom: "F3"),
    .fn(top: "􀊫", bottom: "F4"),
    .fn(top: "􀊰", bottom: "F5"),
    .fn(top: "􀆹", bottom: "F6"),
    .fn(top: "􀊉", bottom: "F7"),
    .fn(top: "􀊇", bottom: "F8"),
    .fn(top: "􀊋", bottom: "F9"),
    .fn(top: "􀊠", bottom: "F10"),
    .fn(top: "􀊤", bottom: "F11"),
    .fn(top: "􀊨", bottom: "F12"),
    .touchId],
    [.topBottom(top: "~", bottom: "."),
    .grid(["!", "1", "", ""]),
    .grid(["@", "2", "", ""]),
    .grid(["#", "3", "ˇ", ""]),
    .grid(["$", "4", "ˋ", ""]),
    .grid(["%", "5", "", ""]),
    .grid(["^", "6", "ˊ", ""]),
    .grid(["&", "7", "˙", ""]),
    .grid(["*", "8", "", ""]),
    .grid(["(", "9", "", ""]),
    .grid([")", "0", "", ""]),
    .grid(["_", "-", "", ""]),
    .topBottom(top: "+", bottom: "="),
    .backspace],
    [.tab,
    .grid(["", "Q", "", ""]),
    .grid(["", "W", "", ""]),
    .grid(["", "E", "", ""]),
    .grid(["", "R", "", ""]),
    .grid(["", "T", "", ""]),
    .grid(["", "Y", "", ""]),
    .grid(["", "U", "", ""]),
    .grid(["", "I", "", ""]),
    .grid(["", "O", "", ""]),
    .grid(["", "P", "", ""]),
    .grid(["", "{", "", "["]),
    .grid(["", "}", "", "]"]),
    .grid(["", "|", "", "\\"])],
    [.capsLock,
    .grid(["", "A", "", ""]),
    .grid(["", "S", "", ""]),
    .grid(["", "D", "", ""]),
    .grid(["", "F", "", ""]),
    .grid(["", "G", "", ""]),
    .grid(["", "H", "", ""]),
    .grid(["", "J", "", ""]),
    .grid(["", "K", "", ""]),
    .grid(["", "L", "", ""]),
    .grid(["", ":", "", ";"]),
    .topBottom(top: "\"", bottom: "'"),
    .return],
    [.leftShift,
    .grid(["", "Z", "", ""]),
    .grid(["", "X", "", ""]),
    .grid(["", "C", "", ""]),
    .grid(["", "V", "", ""]),
    .grid(["", "B", "", ""]),
    .grid(["", "N", "", ""]),
    .grid(["", "M", "", ""]),
    .grid(["", "<", "", ","]),
    .grid(["", ">", "", "."]),
    .grid(["?", "", "", "/"]),
    .rightShift],
    [.globe,
    .control,
    .leftOption,
    .leftCommand,
    .space,
    .rightCommand,
    .rightOption,
    .directions]
    ]
    ForEach(0 ..< allKeys.count) { index in
    let keys = allKeys[index]
    HStack {
    ForEach(keys.map { AnyIdentifiable($0) }) {
    KeyCap($0.value)
    }
    }
    }
    }
    .padding(10)
    .background(Color(white: 0.5))
    .clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous))
    }
    }

    private let fnHeight: CGFloat = 25
    private let defaultWidth: CGFloat = 50
    private let defaultHeight: CGFloat = 50

    struct AnyIdentifiable<T>: Identifiable {
    let id = UUID()
    let value: T
    init(_ value: T) {
    self.value = value
    }
    }

    struct KeyCap: View {
    enum Config {
    case esc
    case fn(top: String, bottom: String)
    case touchId
    case topBottom(top: String, bottom: String)
    case grid([String])
    case backspace
    case tab
    case capsLock, `return`
    case leftShift, rightShift
    case globe, control, leftOption, leftCommand, space, rightCommand, rightOption
    case directions
    }

    init(_ config: Config) {
    self.config = config
    }

    let config: Config

    var body: some View {
    if case Config.directions = config {
    key
    } else {
    key
    .padding(5)
    .background(Color.black)
    .clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
    }
    }

    private struct Grid: View {
    let keys: [String]

    var body: some View {
    let columns = [GridItem(.fixed(defaultWidth / 2), spacing: 5),
    GridItem(.fixed(defaultWidth / 2), spacing: 5)]
    LazyVGrid(columns: columns, spacing: 0, content: {
    ForEach(keys.map { AnyIdentifiable($0) }) {
    Text($0.value)
    .padding(0)
    }
    })
    }
    }

    private struct TopBottom: View {
    init(top: String, bottom: String, alignment: HorizontalAlignment = .center) {
    self.top = top
    self.bottom = bottom
    self.alignment = alignment
    }

    let top: String
    let bottom: String
    let alignment: HorizontalAlignment

    var body: some View {
    VStack(alignment: alignment, spacing: 5) {
    Text(top)
    .padding(.horizontal, 3)
    Text(bottom)
    .padding(.horizontal, 3)
    }
    }
    }

    private struct Arrow: View {
    let key: String

    var body: some View {
    Text(key)
    .font(.caption)
    .frame(width: defaultWidth, height: defaultHeight / 1.7)
    .background(Color.black)
    .clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
    }
    }

    @ViewBuilder
    var key: some View {
    switch config {
    case .esc:
    HStack {
    Text("esc")
    .frame(width: defaultWidth, height: fnHeight, alignment: .leading)
    }
    case .fn(let top, let bottom):
    VStack {
    Text(top)
    Text(bottom)
    .font(.caption2)
    }
    .frame(width: defaultWidth * 1.083, height: fnHeight)
    case .touchId:
    Text("")
    .frame(width: defaultWidth / 2, height: fnHeight)
    case .topBottom(let top, let bottom):
    TopBottom(top: top, bottom: bottom)
    .frame(width: defaultWidth, height: defaultHeight)
    .font(.title2)
    .frame(width: defaultWidth, height: defaultHeight)
    case .grid(let keys):
    Grid(keys: keys)
    .font(.title2)
    .frame(width: defaultWidth, height: defaultHeight)
    case .backspace:
    TopBottom(top: "", bottom: "􀆛")
    .frame(width: defaultWidth * 1.5, height: defaultHeight, alignment: .trailing)
    case .tab:
    TopBottom(top: "", bottom: "􀅆")
    .frame(width: defaultWidth * 1.5, height: defaultHeight, alignment: .leading)
    case .capsLock:
    TopBottom(top: "", bottom: "中/英", alignment: .leading)
    .frame(width: defaultWidth * 2, height: defaultHeight, alignment: .leading)
    case .return:
    TopBottom(top: "", bottom: "􀅇")
    .frame(width: defaultWidth * 1.9, height: defaultHeight, alignment: .trailing)
    case .leftShift:
    TopBottom(top: "", bottom: "􀆝")
    .frame(width: defaultWidth * 2.6, height: defaultHeight, alignment: .leading)
    case .rightShift:
    TopBottom(top: "", bottom: "􀆝")
    .frame(width: defaultWidth * 2.6, height: defaultHeight, alignment: .trailing)
    case .globe:
    Grid(keys: ["", "fn", "􀆪", ""])
    .font(.title3)
    .frame(width: defaultWidth, height: defaultHeight)
    case .control:
    TopBottom(top: "􀆍", bottom: "control", alignment: .trailing)
    .frame(width: defaultWidth, height: defaultHeight, alignment: .trailing)
    case .leftOption:
    TopBottom(top: "􀆕", bottom: "option", alignment: .trailing)
    .frame(width: defaultWidth, height: defaultHeight, alignment: .trailing)
    case .leftCommand:
    TopBottom(top: "􀆔", bottom: "command", alignment: .trailing)
    .frame(width: defaultWidth * 1.4, height: defaultHeight, alignment: .trailing)
    case .space:
    Color.clear
    .frame(width: defaultWidth * 7, height: defaultHeight)
    case .rightCommand:
    TopBottom(top: "􀆔", bottom: "command", alignment: .trailing)
    .frame(width: defaultWidth * 1.4, height: defaultHeight, alignment: .leading)
    case .rightOption:
    TopBottom(top: "􀆕", bottom: "option", alignment: .trailing)
    .frame(width: defaultWidth, height: defaultHeight, alignment: .leading)
    case .directions:
    VStack(spacing: 0) {
    HStack {
    Arrow(key: "􀄤")
    }
    HStack(spacing: 5) {
    Arrow(key: "􀄦")
    Arrow(key: "􀄥")
    Arrow(key: "􀄧")
    }
    }
    }
    }
    }

    struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
    VirtualKeyboard()
    }
    }