This allows fusing nested `.padding` modifiers into a single `div` that sums up padding values from all these modifiers.
Before:
```swift
Text("text").padding(10).padding(20)
```
rendered to this (text styling omitted for brevity):
```html
<div style="padding-top: 20.0px; padding-left: 20.0px; padding-bottom: 20.0px; padding-right: 20.0px;">
<div style="padding-top: 10.0px; padding-left: 10.0px; padding-bottom: 10.0px; padding-right: 10.0px;">
<span>text</span>
</div>
</div>
```
Now it renders as
```html
<div style="padding-top: 30.0px; padding-left: 30.0px; padding-bottom: 30.0px; padding-right: 30.0px;">
<span>text</span>
</div>
```
I hope this approach could be applied to other modifier combinations where it makes sense (in separate PRs).
* Attempt `padding` modifier fusion
* Fix linter warning
* Add a test to verify that fusion works
* Enable fusion of modifiers nested three times
* Filter out empty attributes
* Run snapshot tests only on macOS for now
* Fully exclude snapshot testing on WASI
* Fix `testOptional` snapshot
* Clean up code formatting
This allows writing tests for `TokamakStaticHTML`, `TokamakDOM`, and `TokamakGTK` targets.
The issue was caused by conflicting `ViewDeferredToRenderer` conformances declared in different modules, including the `TokamakTestRenderer` module.
This works around a general limitation in Swift, which was [discussed at length on Swift Forums previously](https://forums.swift.org/t/an-implementation-model-for-rational-protocol-conformance-behavior/37171). When multiple conflicting conformances to the same protocol (`ViewDeferredToRenderer` in our case) exist in different modules, only one of them is available in a given binary (even a test binary). Also, only of them can be loaded and used. Which one exactly is loaded can't be known at compile-time, which is hard to debug and leads to breaking tests that cover code in different renderers. We had to disable `TokamakStaticHTMLTests` for this reason.
The workaround is to declare two new functions in the `Renderer` protocol:
```swift
public protocol Renderer: AnyObject {
// ...
// Functions unrelated to the issue at hand skipped for brevity.
/** Returns a body of a given pritimive view, or `nil` if `view` is not a primitive view for
this renderer.
*/
func body(for view: Any) -> AnyView?
/** Returns `true` if a given view type is a primitive view that should be deferred to this
renderer.
*/
func isPrimitiveView(_ type: Any.Type) -> Bool
}
```
Now each renderer can declare their own protocols for their primitive views, i.e. `HTMLPrimitive`, `DOMPrimitive`, `GTKPrimitive` etc, delegating to them from the implementations of `body(for view:)` and `isPrimitiveView(_:)`. Conformances to these protocols can't conflict across different modules. Also, these protocols can have `internal` visibility, as opposed to `ViewDeferredToRenderer`, which had to be declared as `public` in `TokamakCore` to be visible in renderer modules.
This makes attributes order deterministic and allows testing against HTML renderer output, while currently attributes order is random.
Benchmarks results:
```
name time std iterations
---------------------------------------------------------------------
render Text 9667.000 ns ± 4.35 % 145213
render App unsorted attributes 51917.000 ns ± 4.23 % 26835
render App sorted attributes 52375.000 ns ± 1.62 % 26612
render List unsorted attributes 34546833.500 ns ± 0.79 % 40
render List sorted attributes 34620000.500 ns ± 0.69 % 40
```
Looks like on average there's ~0.2% difference in performance. I was leaning towards enabling sorting by default, but we're benchmarking here only with short attribute dictionaries, I wonder if the difference could become prominent for elements with more attributes. I kept sorting disabled by default after all, but still configurable.
`var html: String` on `StaticHTMLRenderer` was changed to `func render(shouldSortAttributes: Bool = false) -> String` to allow configuring this directly.
* Sort attributes in HTML nodes when rendering
* Make sorting configurable, add benchmarks
* Disable sorting by default, clean up product name
* Fix build errors
Adds `_spi(TokamakCore)` to the modules:
1. TokamakCore
2. TokamakDOM
3. TokamakGTK
4. TokamakStaticHTML
The attribute is applied to:
1. All `View` bodies in TokamakCore — either primitive or regular like `Color`
2. `ViewDeferredToRenderer` bodies
4. `ParentView` `children` members
5. `View` modifiers (such as `_onMount(perform:)`)
6. Other members of types (like `Color._withScheme`, and `_AnyApp._launch`)
The attribute semantics (from my brief testing)
1. It can only be applied to `public` declarations
2. It ensures that every SPI declaration is exposed only by other SPI declarations (i.e. `@_spi(Module) public enum A {}; public var a: A` is illegal)
3. Regularly importing a library prohibits clients from accessing SPI declarations that are not protocol witnesses (with an error).
4. Regularly importing a library "discourages" clients from accessing SPI protocol witnesses by hiding them from the autocompletion suggestions (i.e. users can still access `body` of `Text`, but autocompletion hides it).
5. For a declaration marked with `@_spi(Module)`, a client has to write `@_spi(Module) import Library` in order to normally access such SPI declarations.
* Add '_spi(TokamakCore)' to ideally internal public members.
* Remove `_spi` attribute on '_ConditionalContent'.
* Remove spi from 'Path._PathBox', 'Font._Font', and '_TupleScene.body'.
* Remove spi from types.
* Remove trailing whitespace.
* Apply spi to 'View' modifiers.
* Add _spi imports.
* Introduce 'PrimitiveView'.
* Remove 'PrimitiveView' conformances outside of TokamakCore.
* Fix `PrimitiveView` default implementation.
* Remove "BubbleCore" references.
Resolve#287.
The `checked` attribute is a peculiar one, as any value on it keeps the checkbox checked. Attribute updates in `DOMRenderer` don't handle removals of attributes, but this seems to be the only case where this is relevant. I've added special handling for this attribute and checkbox inputs, and also had to declare `HTMLAttribute.checked` to set `isUpdatedAsProperty: true` on it for it to fully work.
Property assignment does not update SVG elements. We may want to use properties instead of `setAttribute` in the future for optimizations. For now I think that `setAttribute` works in all cases, including SVG, and seems safer to me.
Resolves#278.
* Use setAttribute, not properties to fix SVG update
* Add HTMLAttribute to cleanly apply DOM updates
* Add doc comment to HTMLAttribute
* Update Sources/TokamakStaticHTML/Views/HTML.swift
Co-authored-by: Jed Fox <git@jedfox.com>
* Use static func property in TokamakDOM/TextField.swift
* Make `static func property` public
* Declare `static let value` on `HTMLAttribute`
Co-authored-by: Jed Fox <git@jedfox.com>