Add `TextEditor` implementation (#329)

* Add `TextEditor` implementation

Resolves #173.

* Clean up and bump requirements in the demo project

* Use a single `_tokamak-formcontrol` CSS class

* Add missing CSS class to `TextEditor.swift`

Co-authored-by: Jed Fox <git@jedfox.com>

Co-authored-by: Jed Fox <git@jedfox.com>
This commit is contained in:
Max Desiatov 2020-12-07 21:13:24 +00:00 committed by GitHub
parent 302cd3b108
commit 99581929a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 133 additions and 38 deletions

View File

@ -43,6 +43,8 @@
B5DBA22C24D509B4003D3347 /* RedactDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5DBA22A24D509B4003D3347 /* RedactDemo.swift */; };
B5F2BE032571443D00FB3653 /* PreferenceKeyDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F2BE022571443D00FB3653 /* PreferenceKeyDemo.swift */; };
B5F2BE042571443D00FB3653 /* PreferenceKeyDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F2BE022571443D00FB3653 /* PreferenceKeyDemo.swift */; };
D120FDDB257E7145008FFBAD /* TextEditorDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D120FDDA257E7145008FFBAD /* TextEditorDemo.swift */; };
D120FDDC257E7145008FFBAD /* TextEditorDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D120FDDA257E7145008FFBAD /* TextEditorDemo.swift */; };
D1B4229024B3B9BB00682F74 /* ListDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1B4228E24B3B9BB00682F74 /* ListDemo.swift */; };
D1B4229124B3B9BB00682F74 /* ListDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1B4228E24B3B9BB00682F74 /* ListDemo.swift */; };
D1B4229224B3B9BB00682F74 /* OutlineGroupDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1B4228F24B3B9BB00682F74 /* OutlineGroupDemo.swift */; };
@ -113,6 +115,7 @@
B5C76E4924C73ED4003EABB2 /* AppStorageDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppStorageDemo.swift; sourceTree = "<group>"; };
B5DBA22A24D509B4003D3347 /* RedactDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedactDemo.swift; sourceTree = "<group>"; };
B5F2BE022571443D00FB3653 /* PreferenceKeyDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferenceKeyDemo.swift; sourceTree = "<group>"; };
D120FDDA257E7145008FFBAD /* TextEditorDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextEditorDemo.swift; sourceTree = "<group>"; };
D1B4228E24B3B9BB00682F74 /* ListDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListDemo.swift; sourceTree = "<group>"; };
D1B4228F24B3B9BB00682F74 /* OutlineGroupDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutlineGroupDemo.swift; sourceTree = "<group>"; };
D1C726F224CB63C6003B576D /* ButtonStyleDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ButtonStyleDemo.swift; sourceTree = "<group>"; };
@ -179,6 +182,7 @@
85ED189924AD425E0085DFA0 /* TokamakDemo */ = {
isa = PBXGroup;
children = (
D120FDDA257E7145008FFBAD /* TextEditorDemo.swift */,
D1D6B62224D817350041E1D9 /* GeometryReaderDemo.swift */,
D1C726F224CB63C6003B576D /* ButtonStyleDemo.swift */,
B5C76E4924C73ED4003EABB2 /* AppStorageDemo.swift */,
@ -354,6 +358,7 @@
85ED18A324AD425E0085DFA0 /* SpacerDemo.swift in Sources */,
D1B4229024B3B9BB00682F74 /* ListDemo.swift in Sources */,
D1EE7EA724C0DD2100C0D127 /* PickerDemo.swift in Sources */,
D120FDDB257E7145008FFBAD /* TextEditorDemo.swift in Sources */,
B5F2BE032571443D00FB3653 /* PreferenceKeyDemo.swift in Sources */,
8500293F24D2FF3E001A2E84 /* SliderDemo.swift in Sources */,
85ED18A924AD425E0085DFA0 /* TokamakDemo.swift in Sources */,
@ -383,6 +388,7 @@
85ED18B024AD425E0085DFA0 /* EnvironmentDemo.swift in Sources */,
D1B4229124B3B9BB00682F74 /* ListDemo.swift in Sources */,
D1EE7EA824C0DD2100C0D127 /* PickerDemo.swift in Sources */,
D120FDDC257E7145008FFBAD /* TextEditorDemo.swift in Sources */,
B5F2BE042571443D00FB3653 /* PreferenceKeyDemo.swift in Sources */,
8500294024D2FF3E001A2E84 /* SliderDemo.swift in Sources */,
85ED18B624AD42D70085DFA0 /* NSAppDelegate.swift in Sources */,
@ -546,7 +552,8 @@
DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = "iOS Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.2;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -572,7 +579,8 @@
DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = "iOS Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.2;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -604,6 +612,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 11.0;
PRODUCT_BUNDLE_IDENTIFIER = "dev.tokamak.Tokamak-Native";
PRODUCT_NAME = "TokamakDemo Native";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -630,6 +639,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 11.0;
PRODUCT_BUNDLE_IDENTIFIER = "dev.tokamak.Tokamak-Native";
PRODUCT_NAME = "TokamakDemo Native";
PROVISIONING_PROFILE_SPECIFIER = "";

View File

@ -107,7 +107,8 @@ app.
## Requirements for app developers
- macOS 10.15 and Xcode 11.4 or later.
- macOS 10.15 and Xcode 11.4 or later. macOS 11.0 and Xcode 12.0 or later are required if you're
building a multi-platform app with Tokamak that also needs to support SwiftUI on macOS.
- [Swift 5.2 or later](https://swift.org/download/) and Ubuntu 18.04 if you'd like to use Linux.
Other Linux distributions are currently not supported.
@ -204,7 +205,7 @@ doesn't provide an official build of the extension on the VSCode Marketplace unf
### Modular structure
Tokamak is built with modularity in mind, providing a cross-platform `TokamakCore` module and
Tokamak is built with modularity in mind, providing a multi-platform `TokamakCore` module and
separate modules for platform-specific renderers. Currently, the only available renderer modules are
`TokamakDOM` and `TokamakStaticHTML`, the latter can be used for static websites and server-side
rendering. If you'd like to implement your own custom renderer, please refer to our [renderers

View File

@ -0,0 +1,33 @@
// Copyright 2020 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
public struct TextEditor: View {
let textBinding: Binding<String>
public init(text: Binding<String>) {
textBinding = text
}
public var body: some View {
neverBody("TextEditor")
}
}
public struct _TextEditorProxy {
public let subject: TextEditor
public init(_ subject: TextEditor) { self.subject = subject }
public var textBinding: Binding<String> { subject.textBinding }
}

View File

@ -108,6 +108,7 @@ public typealias SecureField = TokamakCore.SecureField
public typealias Slider = TokamakCore.Slider
public typealias Spacer = TokamakCore.Spacer
public typealias Text = TokamakCore.Text
public typealias TextEditor = TokamakCore.TextEditor
public typealias TextField = TokamakCore.TextField
public typealias Toggle = TokamakCore.Toggle
public typealias VStack = TokamakCore.VStack

View File

@ -21,7 +21,7 @@ extension _PickerContainer: ViewDeferredToRenderer {
AnyView(HTML("label") {
label
Text(" ")
DynamicHTML("select", ["class": "_tokamak-picker"], listeners: ["change": {
DynamicHTML("select", ["class": "_tokamak-formcontrol"], listeners: ["change": {
guard
let valueString = $0.target.object!.value.string,
let value = Int(valueString) as? SelectionValue

View File

@ -24,7 +24,7 @@ extension SecureField: ViewDeferredToRenderer where Label == Text {
"type": "password",
.value: proxy.textBinding.wrappedValue,
"placeholder": proxy.label.rawText,
"class": "_tokamak-securefield",
"class": "_tokamak-formcontrol",
], listeners: [
"keypress": { event in if event.key == "Enter" { proxy.onCommit() } },
"input": { event in

View File

@ -0,0 +1,31 @@
// Copyright 2020 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import TokamakCore
extension TextEditor: ViewDeferredToRenderer {
public var deferredBody: AnyView {
let proxy = _TextEditorProxy(self)
return AnyView(DynamicHTML("textarea", [
"class": "_tokamak-formcontrol _tokamak-texteditor",
], listeners: [
"input": { event in
if let newValue = event.target.object?.value.string {
proxy.textBinding.wrappedValue = newValue
}
},
]))
}
}

View File

@ -32,10 +32,8 @@ extension TextField: ViewDeferredToRenderer where Label == Text {
func className(for style: TextFieldStyle) -> String {
switch style {
case is DefaultTextFieldStyle:
return "_tokamak-textfield-default"
case is RoundedBorderTextFieldStyle:
return "_tokamak-textfield-roundedborder"
case is DefaultTextFieldStyle, is RoundedBorderTextFieldStyle:
return "_tokamak-formcontrol"
default:
return ""
}

View File

@ -17,7 +17,6 @@
import TokamakShim
@available(OSX 11.0, iOS 14.0, *)
struct AppStorageButtons: View {
@AppStorage("count") var count: Int = 0
@SceneStorage("count") var sceneCount: Int = 0
@ -30,7 +29,6 @@ struct AppStorageButtons: View {
}
}
@available(OSX 11.0, iOS 14.0, *)
struct AppStorageDemo: View {
@AppStorage("count") var count: Int = 0
@SceneStorage("count") var sceneCount: Int = 0

View File

@ -17,7 +17,6 @@
import TokamakShim
@available(OSX 10.16, iOS 14.0, *)
public struct GridDemo: View {
public var body: some View {
Group {

View File

@ -23,7 +23,6 @@ struct File: Identifiable {
let children: [File]?
}
@available(OSX 10.16, iOS 14.0, *)
struct OutlineGroupDemo: View {
let fs: [File] = [
.init(id: 0, name: "Users", children: [

View File

@ -24,7 +24,6 @@ struct TestPreferenceKey: PreferenceKey {
}
}
@available(macOS 11, iOS 14, *)
struct PreferenceKeyDemo: View {
@State private var testKeyValue: Color = .yellow
@Environment(\.colorScheme) var colorScheme
@ -144,7 +143,6 @@ struct PreferenceKeyDemo: View {
}
}
@available(macOS 11, iOS 14, *)
extension PreferenceKeyDemo.SetColor where Content == EmptyView {
init(_ level: Int, _ color: Color) {
self.init(level, color) { EmptyView() }

View File

@ -17,7 +17,6 @@
import TokamakShim
@available(OSX 11.0, iOS 14.0, *)
struct RedactionDemo: View {
func title(_ text: String) -> some View {
Group {

View File

@ -0,0 +1,25 @@
// Copyright 2020 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import TokamakShim
struct TextEditorDemo: View {
@State var text = ""
var body: some View {
Text("Word count: \(text.split(separator: " ").count)")
TextEditor(text: $text)
.frame(width: 300, height: 300)
}
}

View File

@ -128,6 +128,7 @@ struct TokamakDemoView: View {
Section(header: Text("Text")) {
NavItem("Text", destination: TextDemo())
NavItem("TextField", destination: TextFieldDemo())
NavItem("TextEditor", destination: TextEditorDemo())
}
Section(header: Text("Misc")) {
NavItem("Path", destination: PathDemo())

View File

@ -96,10 +96,7 @@ public let tokamakStyles = """
height: 100%;
}
._tokamak-securefield,
._tokamak-textfield-default,
._tokamak-textfield-roundedborder,
._tokamak-picker {
._tokamak-formcontrol {
color-scheme: light dark;
}
@ -107,6 +104,11 @@ public let tokamakStyles = """
text-decoration: none;
}
._tokamak-texteditor {
width: 100%;
height: 100%;
}
@media (prefers-color-scheme:dark) {
._tokamak-text-redacted::after {
background-color: rgb(100, 100, 100);

View File

@ -19,7 +19,7 @@ Table columns:
| 🚧 | [Text](https://developer.apple.com/documentation/swiftui/text) | |
| 🚧 | [TextField](https://developer.apple.com/documentation/swiftui/textfield) | |
| 🚧 | [SecureField](https://developer.apple.com/documentation/swiftui/securefield) | |
| | [TextEditor](https://developer.apple.com/documentation/swiftui/texteditor) | |
| | [TextEditor](https://developer.apple.com/documentation/swiftui/texteditor) | |
### Images
@ -51,12 +51,12 @@ Table columns:
### Value Indicators
| | | |
|--- | ------------------------------------------------------------------------------ | :-: |
| | [ProgressView](https://developer.apple.com/documentation/swiftui/progressview) | |
| | [Gauge](https://developer.apple.com/documentation/swiftui/gauge) | |
| | [Label](https://developer.apple.com/documentation/swiftui/label) | |
| ✅ | [Link](https://developer.apple.com/documentation/swiftui/link) | |
| | | |
| --- | ------------------------------------------------------------------------------ | :-: |
| | [ProgressView](https://developer.apple.com/documentation/swiftui/progressview) | |
| | [Gauge](https://developer.apple.com/documentation/swiftui/gauge) | |
| | [Label](https://developer.apple.com/documentation/swiftui/label) | |
| ✅ | [Link](https://developer.apple.com/documentation/swiftui/link) | |
## View Layout and Presentation
@ -72,11 +72,11 @@ Table columns:
### Grids
| | | |
| --- | --------------------------------------------------------------------- | :-: |
|🚧| [LazyHGrid](https://developer.apple.com/documentation/swiftui/lazyhgrid) | |
|🚧| [LazyVGrid](https://developer.apple.com/documentation/swiftui/lazyvgrid) | |
|🚧| [GridItem](https://developer.apple.com/documentation/swiftui/griditem) | |
| | | |
| --- | ------------------------------------------------------------------------ | :-: |
| 🚧 | [LazyHGrid](https://developer.apple.com/documentation/swiftui/lazyhgrid) | |
| 🚧 | [LazyVGrid](https://developer.apple.com/documentation/swiftui/lazyvgrid) | |
| 🚧 | [GridItem](https://developer.apple.com/documentation/swiftui/griditem) | |
### Lists and Scroll Views
@ -96,14 +96,14 @@ Table columns:
| | [Form](https://developer.apple.com/documentation/swiftui/form) | |
| ✅ | [Group](https://developer.apple.com/documentation/swiftui/group) | |
| | [GroupBox](https://developer.apple.com/documentation/swiftui/groupbox) | |
| 🚧 | [Section](https://developer.apple.com/documentation/swiftui/section) | |
| 🚧 | [Section](https://developer.apple.com/documentation/swiftui/section) | |
### Hierarchical Views
| | | |
| --- | --------------------------------------------------------------------------------- | :-: |
|🚧| [OutlineGroup](https://developer.apple.com/documentation/swiftui/outlinegroup) | |
|🚧| [DisclosureGroup](https://developer.apple.com/documentation/swiftui/disclosuregroup) | |
| | | |
| --- | ------------------------------------------------------------------------------------ | :-: |
| 🚧 | [OutlineGroup](https://developer.apple.com/documentation/swiftui/outlinegroup) | |
| 🚧 | [DisclosureGroup](https://developer.apple.com/documentation/swiftui/disclosuregroup) | |
### Spacers and Dividers