Skip to content
82 changes: 71 additions & 11 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,82 @@
# Change log for hatter

## Version 0.3.0 2026.04.19
We used advanced sciences and made hatter even better!

Science was used to make the animations work!
## Version 0.3.0 2026.04.19

### Breaking changes

Also we used science to get IOS to run and install !!!
- `Column` and `Row` constructors now take `LayoutSettings` instead of
`[Widget]`. Use the `column`, `row`, `scrollColumn`, `scrollRow`
smart constructors for the old behaviour.
- `ScrollView` constructor removed. Scrolling is now a property of
`Column`/`Row` via the `lsScrollable` field in `LayoutSettings`,
or use `scrollColumn`/`scrollRow`.
- Container children are now wrapped in `LayoutItem` (with optional
`WidgetKey`) for key-based diffing.
- `Easing`-based tween animations replaced with CSS-like keyframe
animations. Use `linearAnimation`, `easeIn`, `easeOut`,
`easeInOut`, `andThen`, and `lerpStyle` for the new API.
- Removed configurable `soName` from `mkAndroidLib`.

We had some science left and use that to
kill all bugs with pesticides.
### Added

very good.
- `Hatter.Widget.Stack` — z-order overlay container (maps to
FrameLayout on Android, UIView overlay on iOS, ZStack on watchOS).
Includes `wsTouchPassthrough` style field for controlling touch
interception on overlay layers.
- Smart constructors: `column`, `row`, `scrollColumn`, `scrollRow`,
`stack`, `item`, `keyedItem`.
- `LayoutSettings`, `LayoutItem`, `WidgetKey` types for keyed
container children with key-based child matching in `diffContainer`.
- Keyframe animation API: `linearAnimation`, `easeIn`, `easeOut`,
`easeInOut`, `andThen`, `lerpStyle` for composable CSS-like
animation sequences.
- `requestRedraw` API (`Hatter.Render`) for triggering UI re-renders
from background threads. Uses C pthread timer on Android
(non-threaded RTS safe) and platform-native dispatch elsewhere.
- `tiAutoFocus` field on `TextInputConfig` — auto-focus on render
(deferred on Android via `View.post` for attachment safety).
- `Hatter.DeviceInfo` — query device model, OS version, and screen
dimensions on all platforms.
- Re-render UI automatically after `TextInput` value changes.
- `hatter_hs_init` with `RtsConfig` for reliable RTS initialisation
on iOS/watchOS (fixes `hs_init` hang).
- RTS heap limit (`-xr`) on iOS/watchOS real devices to avoid 1TB
`mmap` rejection.
- Build hatter as a normal cross-compiled Haskell package via
`collect-deps.nix` / `cross-deps.nix`.
- Share pre-compiled hatter objects across all Android ABI builds.
- `-split-sections` + `--gc-sections` for smaller Android `.so` files.
- Node ID reclamation via free stack on all platforms.

I can make ask claudes to make a nice overview in here.
but fuck that,
### Fixed

better to make u laugh traveler.
- First-render animation bug: tweens now register from zero origin
on initial render, not only on re-render.
- Animated widget toggle-back bug: animation config preserved when
toggling an `Animated` wrapper off then back on.
- Key-based child diffing prevents cascading native view destruction
when inserting/removing children mid-list.
- Index-based default keys replace content-based `inferKey`, avoiding
hash collisions for identical widgets.
- `Styled` wrapper now reapplies style when the child widget changes
type (e.g. `Text` to `Button`).
- Android `destroy_node` detaches view from parent before freeing JNI
refs (fixes orphaned native views).
- ScrollView SIGABRT on Android when mixing `TextInput` with other
widgets — children now wrapped in inner `LinearLayout`.
- Android `TextWatcher` re-entry crash prevented by guarding against
redundant `setText` calls.
- In-place diff for `Text`/`Button` widgets preserves native IME
connection on Android.
- armv7a OOM from duplicated `registerForeignExports` `.init_array`
entries.
- iOS/watchOS cross-build: drop `deriving stock` on `WidgetKey`.
- iOS `hs_init` hang resolved via `hatter_hs_init` with explicit
`RtsConfig` and null-terminated argv.
- Swift type inference errors on Xcode 16.4.
- `os_log` `CVarArg` conformance on iOS — use `String(describing:)`
for pointer values.
- GNU `libffi` built from source for static iOS/watchOS bundling.

## Version 0.2.0

Expand Down
9 changes: 6 additions & 3 deletions hatter.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,8 @@ library
containers < 1,
bytestring < 1,
transformers < 0.7,
time
time,
unwitch >= 3.0.0 && < 4
c-sources:
cbits/android_stubs.c
cbits/platform_log.c
Expand Down Expand Up @@ -191,7 +192,8 @@ executable redraw-demo
test
build-depends:
hatter,
text
text,
unwitch >= 3.0.0 && < 4

executable confetti-repro-demo
import: common-options
Expand Down Expand Up @@ -250,4 +252,5 @@ test-suite unit
text,
bytestring,
directory,
filepath
filepath,
unwitch >= 3.0.0 && < 4
14 changes: 13 additions & 1 deletion nix/cross-deps.nix
Original file line number Diff line number Diff line change
Expand Up @@ -228,10 +228,18 @@ WRAPPER
});
} else {};

unwitchOverride = self: super: {
unwitch = self.callCabal2nix "unwitch" (builtins.fetchTarball {
url = "https://github.com/jappeace/unwitch/archive/2759bdd153f293e0e6524d0170e861e51302caa4.tar.gz";
sha256 = "sha256:BGxZ1CQGIYP/gg/J9jua2/wSEH4qq7bW91qooNELUlI=";
}) {};
};

defaultOverrides =
let
common = pkgs.lib.composeManyExtensions [
vectorOverride
unwitchOverride
thPackageDbOverride
thIservOverride
hatterOverride
Expand Down Expand Up @@ -284,9 +292,13 @@ WRAPPER
# so its .a and .conf are available for linking.
hatterDep = if hatterSrc != null then [ crossHaskellPkgs.hatter ] else [];

# Hatter's own non-boot dependencies — must be collected so hatter's
# .conf can resolve them (collect-deps doesn't follow propagatedBuildInputs).
hatterOwnDeps = [ crossHaskellPkgs.unwitch ];

in import ./collect-deps.nix {
inherit pkgs ghc ghcPkgCmd;
deps = resolvedDeps ++ hatterDep;
deps = resolvedDeps ++ hatterDep ++ hatterOwnDeps;
mainLibPnames = if hatterSrc != null then [ "hatter" ] else [];
iservProxy = iservWrapper;
}
4 changes: 4 additions & 0 deletions nix/hpkgs.nix
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,9 @@ pkgs.haskellPackages.override {
# NB this is a bit silly because nix files are now considered for the build
# bigger projects should consider putting haskell stuff in a subfolder
hatter-project = hnew.callCabal2nix "hatter" ../. { };
unwitch = hnew.callCabal2nix "unwitch" (builtins.fetchTarball {
url = "https://github.com/jappeace/unwitch/archive/2759bdd153f293e0e6524d0170e861e51302caa4.tar.gz";
sha256 = "sha256:BGxZ1CQGIYP/gg/J9jua2/wSEH4qq7bW91qooNELUlI=";
}) {};
};
}
39 changes: 37 additions & 2 deletions nix/ios-deps.nix
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,39 @@
, consumerCabalFile ? null
, consumerCabal2Nix ? null
, hpkgs ? (_: _: {}) # consumer haskellPackages overrides
, hatterSrc ? null # hatter source tree (builds hatter as a normal dep)
}:
let
pkgs = import sources.nixpkgs {};

unwitchOverride = self: super: {
unwitch = self.callCabal2nix "unwitch" (builtins.fetchTarball {
url = "https://github.com/jappeace/unwitch/archive/2759bdd153f293e0e6524d0170e861e51302caa4.tar.gz";
sha256 = "sha256:BGxZ1CQGIYP/gg/J9jua2/wSEH4qq7bW91qooNELUlI=";
}) {};
};

# Build hatter as a regular haskellPackages derivation from local source.
# Executables and tests are stripped to avoid pulling in test-framework deps.
hatterOverride = self: super:
if hatterSrc != null then {
hatter = pkgs.haskell.lib.overrideCabal
(self.callCabal2nix "hatter" hatterSrc {})
(old: {
postPatch = (old.postPatch or "") + ''
sed -i '/^executable /,$d' hatter.cabal
sed -i '/^test-suite /,$d' hatter.cabal
'';
doCheck = false;
});
} else {};

nativeHaskellPkgs = pkgs.haskellPackages.override {
overrides = hpkgs;
overrides = pkgs.lib.composeManyExtensions [
unwitchOverride
hatterOverride
hpkgs
];
};

ghc = nativeHaskellPkgs.ghc;
Expand All @@ -30,7 +57,15 @@ let
haskellPkgs = nativeHaskellPkgs;
};

# When hatterSrc is provided, add the hatter package to the collected deps
# so its .a and .conf are available for linking.
hatterDep = if hatterSrc != null then [ nativeHaskellPkgs.hatter ] else [];

# Hatter's own non-boot dependencies — always included so mkIOSLib's
# raw GHC invocation can find them even without a consumer cabal file.
hatterOwnDeps = [ nativeHaskellPkgs.unwitch ];

in import ./collect-deps.nix {
inherit pkgs ghc ghcPkgCmd;
deps = resolvedDeps;
deps = resolvedDeps ++ hatterDep ++ hatterOwnDeps;
}
1 change: 1 addition & 0 deletions nix/ios.nix
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ let
lib = import ./lib.nix { inherit sources; };
iosDeps = import ./ios-deps.nix {
inherit sources consumerCabalFile consumerCabal2Nix hpkgs;
hatterSrc = ../.;
};
in
lib.mkIOSLib {
Expand Down
Loading
Loading