[X] make peace with cosmic text
[X] clicks and inputs
[X] skip rerenders
[X] partial relayouts and rect creations
[X] to be completely transparent, the sibling stuff should also be rewritten without the thread local stack. Don't wanna think about it right now, but I'm almost sure it's ez.
[X] scrolling:
[X] min max scrolling
[ ] same thing on stacks and textcontent
[ ] more scrolling (won't do this):
This whole stuff with content_bounding_rect and stuff is a tragedy. I think it should use content size instead or something.
[ ] test some more weird combinations
[ ] scroll bars??
[ ] scroll stays reasonable when resizing
[ ] an optimized path for recalculating just the positions instead of doing a normal partial relayout
[X] Rename ax -> axis
[ ] Rename node <-> i everywhere?
[ ] keyboard navigation?? just move focused around? and make enter + focused emit is_clicked? think about it
[ ] animations
[ ] sibling keys example
[X] custom rendered rects
[X] widgets (aka subtrees)
[ ] partial declarative mode? now "reactivity"
[ ] purging/cleanups, especially for twins and siblings
[ ] cosmic text text edit
[ ] 9 patch rects
[X] crate structure
[X] log/warnings, and add warnings for bad use of twins
[X] docs
[ ] more docs
[ ] having a small radius like 0.0 - 0.5 makes the whole rect transparent o algo
[ ] Important todo: there should be a single "source of last text". keeping a last_hash and a last_static_ptr is obviously bad.
[X] Is it possible for a node to be its own parent? that would probably cause a ton of aliasing and bugs.
[X] Gotta do another pass over the click detection crap. currently the debug empty rectangles are absorbing clicks, I think.
[X] I think v_stack ordering has been opposite the whole time? no, it's probably flipped from when we made for_each_child!() go backwards.
[ ] gpu rect buffer should be dynamic sized? I guess just check the space on every prepare() and recreate it if needed
[ ] Image rects are still missing from cosmetic updates
[ ] Tree changes are disregarding the relayout chain stuff
[ ] I think text attr changes will need their own cosmetic update like thing?? maybe?
[ ] Text changes should only cause relayouts if their container is fit_content
[ ] Configurable color/depth attachments
[ ] Configurable Z levels (maybe with debug warnings if z ends up outside of bounds)
[ ] For harmony, all the click functions could be moved to uinode, like inner_size() render_rect() etc all are.
(the downside here is having to unwrap. is_clicked can just return None. Could make a NoneNode that returns false on everything, but that seems kind of stupid.)
[X] Fix hovering
[ ] you should be able to use the position system for placing stuff freely. (anchor, own center, if that's all separate, etc)
[X] Better tree links + solve that order skissue
[ ] Pixel perfectness
[ ] Text uses proper z
[X] maybe, scan for mouse hits on every mouse movement and after relayout, then use the last result for everything else (all clicks, hover, etc)
[ ] Ui::new() without immediately linking to the window and gpu
[ ] Separate Ui and UiRenderer?
[ ] Software renderer lul
[ ] Muh tests
[ ] Moving the window with super+arrows still doesn't reset hover animations right. this is legit irrelevant though. Actually it's still wrong on resize too.
[ ] Dvec2 hell
[ ] Rects with less crap for mouse hits
[ ] Images are pretty bad right now lol. Rewrite it all and add scrolling.
[ ] Something about "infinite" Size instead of fitcontent? for stacks? or maybe stacks work differently intrinsecally?
I think the point was that fitcontent stacks that place everything at the middle don't really need to resize themselves because their bounds aren't relevant.
for most other kinds of stacks that wouldn't be true though.
maybe just add an exception to the fitcontent chain thing?
[ ] "Parent device is lost" crash, maybe we did have to recreate the wgpu state on resume after all
[ ] maybe some timings in log::trace() would be cool
[X] ui.anon(params) (= ui.add_anon().params(params).place())
[ ] Rename "add" to "build"?
[ ] rename 'a to 'ui everywhere
[ ] use keys instead of slab i's in UiNode and friends?
------------
The actually good way to get ids: use the track_location hash, and mix it with a FULL TREE hash, ONLY IF IT'S COLLIDING.
This still means adding a hashmap of subtree ids. (I guess hashset)
-------------
The scrolling part is just all terrible 2bh2bh
There should be a path to do cosmetic updates like scrolling or button darkening without rerunning the ui update (aka not with needs_update())
Really? No.
Because that would have to be exposed in the main loop.
The shortcut that scrolling is using right now (doing everything on the event) is better, because it works with no integration needed in the winit loop. Maybe other cosmetic updates should do that too.
Only downside is that we HAVE NO IDEA how those events actually work.
How many scroll event can we get in the time between two frames? 1? 2? 100?
If we request_redraw() on each of those 100 hypothetical events how many RedrawRequested will we actually get?
The other way is to do all that stuff on prepare(). After all, it's just cosmetic, right?
There was some kind of philosophical argument that the nodes or at least the rects should always reflect the true state, regardless of the rendering. Perhaps relevant for tests or something.
I'm ready to give up on that though.
This is only an issue for scroll, though.
For click and hover it already works ok? I think. it should set need_rerender but not needs_update().
the only_adjust_scroll thing might be wrong. children need to be replaced from scratch. Only way to not do that is to go back to the recursive_adjust_scroll style thing.
way more important, skip the glyphon prepare().
nevermind, looks like it's auto-skipping. should still skip it, but it's not the reason why it lags.
the reason is that when new pieces of text get scrolled in, glyphon takes
100 M I L L I S E C O N D S
0
0
M
I
L
L
I
S
E
C
O
N
D
S
(0.1 SECONDS!!!!!!!!)
to run prepare().
-----------
Place() needs to be twin-aware somehow.
Probably just panic. It's already pretty panic-happy.
Otherwise, it could create duplicates, as if you had add()ed the same thing multiple times.
Otherwise, go back to an older style with the NodeParams on the stack, and something different for text and image.
Consider that the text and image API is bad and slow either way.
-----------
Text size:
when you "fit content" horizontally, how do you know if you should do line breaks? how many?
just have an attribute "single line".
but how many?
just fill the whole proposed size horizontally.
Maybe just remove FitContentOrMinimum tbh tbh.
Track caller is still hitting twins a lot if someone puts it in a function.
Our functions have #[track_caller] but others won't.
[ ] crash when resizing window to 0
[X] wgpu errors
[ ] store track caller in debug mode, for normal nodes as well
[ ] No more Option<usize>
[ ] When does resolve_hover set new_input?
This is hard, because it depends on what the user is doing.
For the current sliders, which use is_dragged, it's when something is being held, but otherwise it's independent even of whether it's hitting a node or not.
We always know which is_dragged, is_hovered calls are coming, but that's from the last frame only.
It could be ok to assume it from last frame, but it'd be a bit weird. and what about on the first frame?
I guess this all falls under "update driven animations" in a sort of way?
Should add more options or at least docs.
[X] Isn't the current current_tree_hash terrible? it only considers the parents, not the child N, so it probably relies on twins after that. At that point just get a good hash including the child N, and then also the module/line/column while we're at it (unless it's expensive)
[X] All good progress tbh, but now all the anon nodes trigger the tree_changed thing. (probably ok?)
------------
About subtrees:
I guess the subtree id is either explicit or implicit in the same way as node ids.
It could take an Option<Id> maybe. Since there's no checking for input, maybe it's never relevant to have an explicit ID.
A bunch of functions need to be subtree-aware.
[X] Add the function
[X] Add the stack in the thread local thing. remember about canceling
[X] Make the functions subtree-aware. Just slapped it on NodeKey::id().
let wheel_info = ui.get_node(OKLAB_HUE_WHEEL)?.render_rect();
2) In the main function, store the name of the subtree. Then in whatever other helper function, re-enter that subtree.
3) require an explicit subtree name passed from outside when constructing the ColorPicker state, then enter it in both the main functions and the other one.
Note that all kinds of explicit, stored, re-entered subtree Ids would work differently though. You'd use just that one u64 as a hash, not the whole tree.
From outside, it would be weird that the color picker needs an Id, and other stuff doesn't. The color picker has state, I guess, and that's kind of related (other functions can be impl'd on that state and still want to refer to the nodes without an easy way to do it), but not THAT related. Also you could have state and impl stuff on it that doesn't care about the nodes.
From inside, the temptation of just doing it the way that we already do it is very strong. And whatever complicated thing we'd tell people to do, it'd be very easy to just forget to do it.
2 and 3 are worse in the sense that 1) you have to remember to store the key, and then you also have to remember to re-enter it.
I guess the good thing is that we don't have to choose. Once non_anon_subtree is there, you can do any of those things.
[ ] Current tree hash could limit itself to the current subtree, but it doesn't sound super essential.
[ ] Subtree key should probably be its own type, maybe? Won't bother making another macro for it now.
------------
[X] Lets rip out the click helper thing.
As a bonus, putting the new_input stuff there could help, maybe? Still depends if we want to make it per-node or not. But that's such a fat amount of stupid annotations.
[X] Still fixing the painter for new winit and stuff
[X] Resolve hover after resizing isnt right anymore
[X] Merge prepare and render already
[X] Complain fruitlessly about the unwrap() shit
From first principles:
we already know this. 2 modes.
- [ ] awake: use the sleep thing, or try using the winit WaitUntil.
- [X] sleepy: call request_redraw() after an input event which gets absorbed, or after a redrawrequested that leaves an alive anim_time after or similar. See in detail how resolving events works to see how to trigger it.
What about external events?? Maybe that was a meme after all.
Nah, it's just done at the "just skipping update()" level instead of the winit level.
Or maybe you can use the winit custom events? from another thread? oalgo?
- [X] sleepy with input on canvas
- [ ] sleepy with external events
if we add the "please render again/please update and render again" signal, then "awake" is just spamming the update again signal every time.
Cpu usage starts at zero but then stays at 0.30.
When it's just animations, sleep mode should ro many redraws but skip updates.
For sleep mode, it's definitely better to always run update, and only skip it if it's animation only.
for awake mode, you always run update anyway.
I think it's completely broken right now lol, and only works thanks to the animations forcing an update.
... better now prob...
but still broken, I think because of end_frame_resolve_inputs
might either run something equivalent to that on handle_events,
there's totally the option of adding one frame of latency too
when thinking about this in the non-sleepy mindset it was fine that way
Oh, maybe I know... you still delay that update() until the next redrawrequested cuz why not, and then it works
[X] Maybe too many Instant::now()s now?
Nah
Sleep mode
update code itself should be ok, but finish_tree also does relayout, and when you resize you need relayout without redeclaration
I guess handle_events could do the relayout immediately?
Sure.
Only downside is we might be doing relayouts twice on resize, since finish_tree definitely still needs it.
The other option is to run the update code anyway when resizing. Seems like it could be useful tbh.
[X] Don't we need to rerun resolve-hover or something, too? some node might be moved and end up under the mouse.
Was already happening.
-----------
magic doc block (forsenCD):
/// # use keru::*;
/// # pub struct State {
/// # pub ui: Ui,
/// # }
/// #
/// # impl State {
/// # fn declare_ui(&mut self) {
/// # let ui = &mut self.ui;
/// #
/// #
/// # }
/// # }
/// # #[node_key] pub const CHILD: NodeKey;
------------
double place()s on the same key is actually terrible. completely unpredictable things happening. maybe even panics or infinite loops. can't really tell.
After moving refresh to place, it should be easy to just panic.
That was mental illness
currently the very confusing mechanism is this:
- the non-retained-mode parent gets all his shit cleared on his add_or_update() (though maybe it should be moved to place()... but it doesn't matter)
- the retained-mode guy does add() and place(), which clears his own children, and places it as a child of the parent again
what assume_unchanged() (instead of add() + place()) should do is this:
- not clear his own children
- add himself to its old parent in the same position as he was before!
in practice, you just call assume_unchanged() in the same tree-level as you'd have called place(), so you can just add it regularly.
but this seems a bit scary, somehow.
but it's the only way: "in the same position as before" means nothing, because all the other children might change
then I guess it has to do the same shit as place(), basically.
How could it not depend on the keep_unchanged() being on the same level as the regular place()?
By being the same call, obviously. But how tho?
Can't early return from inside the closures.
Could do something stupid like assigning the parent.
But the obviously better solution is to just not run the closure. (that's probably what the crochet thing was doing)
```rust
self.ui.place(TOOLS_PANEL).assume_unchanged_if(changed).nest(|| {
self.ui.v_stack().nest(|| {
self.ui.place(BRUSH);
self.ui.place(ERASER);
});
});
```
assume_unchanged_if() converts the Parent into a RetainedParent, which has a "changed" field for the bool, and a different nest() function that just doesn't run the closure.
Not everything that you'd possibly want to skip is inside a nest()? But it's probably ok.
Actually: place() is the last call to do what add() and place() usually do (clearing children). I guess you could put them into last_frame_children and undelete them in assume_unchanged_if(), but that's cringe.
```rust
self.ui.place_and_assume_unchanged(TOOLS_PANEL, changed).nest(|| {
self.ui.v_stack().nest(|| {
self.ui.place(BRUSH);
self.ui.place(ERASER);
});
});
// assume_unchanged_if(): use a threadlocal stack to toggle on and off a retained mode.
// the calls to place() don't clear their children. Calls to nest() don't execute their closures.
// in this way, only the first-level calls to place() actually happen, which is what we wanted.
self.ui.assume_unchanged_if(unchanged).block(|| {
self.ui.place(TOOLS_PANEL).nest(|| {
self.ui.v_stack().nest(|| {
self.ui.place(BRUSH);
self.ui.place(ERASER);
});
});
});
// this is good, I think.
```
-------------
It's ok now, but the main skissue is that it be misused, ie someone can call keep_whole_subtree_unchanged() but then not return and also run the whole tree.
what would even happen? I guess twin everything and end up with two copies? should try it.
since we have to set a retained flag anyway, can't it just see that there is a retained copy and do nothing?
also, another misuse is calling keep_whole_subtree_unchanged() not at the right level as the old place() was.
you get the same situation by mixing up which key to pass to keep_whole_subtree_unchanged(), ie if we passed "brush" to it.
what would even happen? bad stuff, I think.
How does it get removed? automatically, I think.
- if the parent doesn't get called, it's out-of-tree anyway (but what about the text areas?????)
- if there's an if below, then the if's condition should be part of the `changed` condition...
Right now there's good place for assume_unchanged to do anything at all.
An ok trick could be that assume_unchanged(KEY) gets the pointed node, goes to its current parent, and adds KEY (or the slab i) to a list of "static children" that don't get reset.
For text areas, I guess you could go through the subtree and mark all of them as fixed.
------------
with the current system, it's impossible to have proper alpha blending!
the render order is:
1) grey background square
2) small white ring
3) oklab square
with the z buffer, the oklab square can be behind the small white ring. But it can't be blended properly. By the time it's his turn, the small white ring already blended with the grey background, and that pixel is a high z, high alpha grey-white value. The oklab square sees that it's high Z, high alpha and just leaves it as it is.
I remember hearing about the "Order independent transparency" problem somewhere. I guess this is it.
I guess in theory it could keep two buffers and blend them after.
--------------
[X] dont keep rerendering
[X] slider no longer werks?
----------
advanced layout stuff
- remove some duplication
- anchors like in godont
- "PositionBy" center or edges
----------
[X] last_proposed_size
just set it when proposing the size oalgo
----------
[X] why is clicking so complicated?
I think this is how it should be
- clicks
on every click
- add it to clicks
- replace hold
at the end of the frame,
- clear clicks
- hold will be something like { x, y, id, 1 or more buttons }. If the mouse is not in the rect anymore, clear it. Otherwise, remove the buttons that aren't kept anymore. If there are no more buttons, just remove it.
Drags?
Drags are kind of like hold, but not really, because there can be multiple drags in one frame if the user is a real gamer.
Also, you can move the mouse and press/release left/right buttons at different times.
But maybe this is true: add Drags at the end of the frame and at every mouse button release.
The end-of-frame one is the one that's the same as the single held.
The truth nuke is that held should work in that way too. (There can be a simpler api that just checks the end-of-frame one, maybe.)
No, "single currently held node" can be separate. Those partial helds are literally the same thing as drags, you just ignore the diff.
Absolutely not, hold ends when the mouse exits the rect, drag ends only on release.
are they also the same thing as clicks? that's the real question.
I think not.
What about this:
- store click DOWN events.
- whenever a click UP or an exit-node event happes, it matches with a click down event, which is the last one matching some conditions.
then, it combines the data from the two events to push either a "held_period" or a "drag_period" in last_frame_held_nodes etc.
click up:
- it matches the latest clickdown with the same button.
- if the last click down has the same rect ID, push both a held_period and a drag_period.
- if it has a different rect ID, push just a drag_period (because the held_period is already over and was pushed by the exit event).
exit rect:
- it matches the latest clickdown with the same button and pushes a held_period.
how does this work with non-absorbed clicks? if a click down is out of a rect and the click up is in, I guess that might look for some really old clickdown.
I guess the simplest way is to just mark on the clickdown events the fields held_resolved and drag_resolved.
That's probably also good for click-on-release, maybe? dunno. It probably works, if the clickdowns get retained for multiple frames. Which has to happen anyway.
I guess there's last_frame_clickdowns and unresolved_clickdowns, and at the end of the frame the new ones get dumped onto the old ones.
And the old ones also get removed when they resolve properly (drag/clickup).
but how does it work with click releases that are out of the window?? Dunno, maybe winit already solves that for us though. Aka it passes us the clickup anyway.
On deeper inspection, looks like the thing where holding a button ends when exiting the element isn't really relevant.
But it's still relevant for click-on-release, right?
I forgot, how do we check if a button if currently still being held at the end of the frame? I guess check both last_frame_click_presses and old_unresolved_click_presses?
Why not have a single vec and keep the frame number on every press?
It's automatically sorted, so not a big deal.
// we don't remove, it right? so that click-on-press people can look at click_presses and still find them.
// then at the end of the frame we go through click_presses and remove the resolved ones?
// but then we also have to mark the unresolved ones as "old" so click-on-press people can ignore them.
// is that overcomplicated?
// it's either that or keep a separate vec for last frame presses, I think.
// maybe separate vec is cleaner.
// then at the end of the frame clear all of the separate vec presses,
// at the end of the frame we go through click_presses and remove the resolved ones
// no, let's try a single vec.
Wrong again, lol.
The end of the frame should push a drag/hold event immediately for each of the unresolved presses.
It should also update the timestamp and position. Maybe it could be useful for someone to keep the original pressed time/pos? If yes, add a "last_checked" pos or something.
I think not tbdesu. Who cares.
-----------
Should decide something for clickable/visible rects.
- everything is click-opaque, I think. A clickable rect that doesn't absorb the click sounds useless. And it can probably be added later if needed.
- a normal visible rect is always clickable and click-opaque.
- when invisible rects get rendered anyway (debug mode), they have to be marked as non-clickable and skipped.
- custom rendered rects should be clickable, so they need to be added into a separate vec? but only in non-debug mode, otherwise you have both that one and the greenscreen rect in the regular vec. Sounds complicated. Maybe just do the separate datastructure for click detection already? Don't really feel like it.
- outline only rects can probably just be non-clickable for now, but we'll add clickable later, maybe. To be fair outline only should go into Shape instead of being a separate thing.
------
Some typing:
- implement circles and rings by abusing rounded rectangles
- make click detection aware of circles and rings
- readd custom rendering
for real reactivity, you would do this:
- both the `count` and `show` inside changewatchers
- the struct implements a trait which allows the declare_ui() function to know if some of its data has changed
- it does nothing if nothing changes
- more practically, you just wrap the whole count + show struct in a changewatcher.
we might be on to something here, but there has to be a distinction between the kind of skipping that leaves the whole subtree as it was before, and the kind of skipping that removes the subtree, obviously.
if let Some(counter_state) = self.counter_state.if_changed() {
ui.v_stack().nest(|| {
if self.show {
ui.add(&increase).static_text("Increase").set_color(color);
ui.add(&LABEL).dyn_text(self.count.if_changed());
ui.add(&decrease).static_text("Decrease");
}
ui.add(&show).static_text(&show_button_text);
});
} else {
// set a flag on the Node or in Ui that just doesn't clear the tree.
// the main thing would be to just not clear the parent-child relation.
// there's surely some details but should be ez enough.
}
But can it be done automatically??? we just need access to the ui?
if ui.change_watcher_guard(counter_state) {
// normal declarative code
}
impl Ui {
fn change_watcher_guard(reactive_state: impl ChangeWatcher) -> Option<T> {
if reactive_state.is_none() {
// set the various flags
}
return reactive_state;
}
}
That's good, I think.
(also, the ideal situation would this to cover 100% of cases perfectly, and use that as justification to remove all diffing between old and new params, etc. But probably that won't happen. so maybe another hint is needed, to let the tree know that since we're guarded by a changewatcher thing, we can skip diffing??? )
What if you do a little bit of nesting?
With what we said so far, you just skip the whole subtree, which is obviously wrong?
As for the recurring theme, you don't literally skip executing the code. You should just set some flag that turns the add() calls into nothing, not even hashmap access or diffing.
Ahhh, but what about this:
If you want to opt in into reactive shit, instead of passing a NodeParams to add(), you pass an Option<NodeParams> that is computed from your state, and that gives None if state.peek_changed() == false.
Is this really ergonomic? you have to do that for every add().
With the closures, you could do things like
ui.with_diffed_state(|| {
ui.with_reactive_state(|state1| {
ui.with_reactive_state(|state2| {
}
});
});
to set and reset flags as needed. doesn't look too bad 2bh.
add( ) would be kind of a mess, with different behavior depending on all these flags and shit, but it wouldn't be too bad I guess.
(the flags would be thread local similar to the parents.)
The aesthetics are bad, but obviously widgets would hide most of those calls.
...another way, the nodeparams can have a flag saying whether they changed or not, and when you use builder functions to build them, you can use a version of the function that takes a changewatcher<Color/f32/whatever> and propagates the changed info inside the params. Isn't this way better? Maybe.
The bump in complexity in NodeParams would bring the questionable benefit that at that point, since it's so complicated, you might as well add the lifetime soup stuff too.
How would this look?
#[node_key] const INCREASE: NodeKey;
let increase = BUTTON.key(INCREASE).color(color);
#[node_key] const DECREASE: NodeKey;
let decrease = BUTTON.key(DECREASE);
#[node_key] const SHOW: NodeKey;
let show = BUTTON.key(SHOW).color(Color::RED).static_text(&show_button_text);
It's exactly how we already saw with strings, with the difference that we're writing in the Params, not in the node, so we can't do the None thing to skip eagerly evaluating stuff that we don't need. Maybe it sucks then?
The reactive version would have to be a separate function:
fn reactive_color(&mut self, color: ChangeWatcher<Color>) {
self.rect.is_changed = color.is_changed();
self.rect.color = color.value;
}
but it's still true that you always need to have a Color there even if nothing changed.
Honestly, this sucks.
We could go the opposite direction doebeit, and do all those kind of updates directly on the nodes. At that point you could do the Option/if_changed() thing that we're doing for the string.
Right now we consider it a big negative that that string function if stuck to the add() call, because it mixes up layout and aesthetics/updates. You could take it outside like this:
ui.select_node(&LABEL).color(...).text(...)
but that's one extra useless hashmap/slab access, and we still managed to separate default aesthetics from updates, which we decided was bad ages ago.
Also, I don't know how I managed to miss that for 17 months, but the guy on fivechuds was right about multiple observers.
Yikes!
However, this isn't an issue for that closure idea, because you can probably reset the changed flag at the end of the closure instead of on every read.
So despite everything I think we're back to this:
ui.with_reactive_state(state2, || {
});
});
});
fn with_reactive_state<T>(&self, state: &mut ChangeWatcher<T>, block: impl FnOnce()) {
if state.is_changed() {
thread_set_update_on_every_add_because_something_changed(self);
} else {
thread_set_do_nothing_on_every_add_because_nothing_changed(self);
}
children_block();
thread_local_clear_flag();
state.clear_changed();
}
The way it's written, it will probably not work because of borrows?
Maybe not, idk.
Even with that, it wouldn't work:
ui.with_reactive_state(state, || {
});
// clear changed after the closure
ui.with_reactive_state(state, || {
// ???
});
You need to clear_changed( ) at the end of the frame only. no shotcuts.
But that's impossible to do in a "the user is still the owner of its state" system. the Ui can't stash references to the user state and clear them at the end.
I think this whole idea is doomed. Rather, just go into a partial declarative/partial retained direction that doesn't rely on stat changes, but on explicit events for a few important things, and diffing for everything else.
The current dyn_string stuff with the multiple observer issue should just get rooted. Who cares? just diff if or hash it.
It's probably fine to do that whole flag thing as a optional helper.
Besides, the real pressing skissue is how fucking ugly it looks to have text outside of nodeparams.
What if you did this:
```rust
#[node_key] const INCREASE: NodeKey;
#[node_key] const DECREASE: NodeKey;
#[node_key] const LABEL: NodeKey;
#[node_key] const SHOW: NodeKey;
// create or update the nodes here
ui.add(INCREASE, params).text(text).image(image).whatever(...);
ui.add(DECREASE, params).text(text).image(image).whatever(...);
ui.add(LABEL, params).text(text).image(image).whatever(...);
ui.add(SHOW, params).text(text).image(image).whatever(...);
// here, just set the parent/child relations on the already-updated nodes.
ui.v_stack().nest(|| {
if self.show {
ui.place(INCREASE);
ui.place(LABEL);
ui.place(INCREASE);
}
ui.place(SHOW);
});
```
I think this is big, if true.
Maybe add() could be called place() or something.
But it definitely means more hashmap accesses.
It should be possible to have ways to create and add in the same line, if preferred: just
```rust
#[node_key] const INCREASE: NodeKey;
#[node_key] const DECREASE: NodeKey;
ui.add_node(DECREASE, params).text(text);
#[node_key] const LABEL: NodeKey;
ui.add_node(LABEL, params).text(text);
#[node_key] const SHOW: NodeKey;
ui.add_node(SHOW, params).text(text);
ui.v_stack().nest(|| {
// ^ I guess this one is special, and it both creates the node and places it?
if self.show {
ui.add_node(INCREASE, params).text(text).place();
// ^ create the node inline ^ UiWithNode::place()
ui.place(LABEL);
// place the node, if it exists. Isn't this a dynamic typing/runtime check type thing doebeit?
// doesn't sound that bad.
ui.place(increase);
// if ui.create_node returned a POD thing with the slab i, that would make it """REASONABLY""" sure that you actually created the node. You could still have multiple Ui objects or something?
ui.add_node(INCREASE, params).text(text).place().is_clicked() {
self.count += 1;
};
// this is actually kind of awful, because it relies on the place() hidden in the middle to call to actually show up on the screen and do anything.
// however, if you forget the place(), you can't do is_clicked() either, so it's not that hard to notice.
}
ui.add(SHOW);
});
// or the opposite?
ui.place(INCREASE).update(params).text(text).is_clicked() {
self.count += 1;
};
// this wouldn't work with nest() because of the classic issue. above, place() was also converting from an updateable UiWithNode to a nestable Parent.
// clicks can work on anything, but not nest because of the borrowing skissues.
```
Should try the lifetime soup anyway. It probably wasn't that bad. But that Impl Display + Hash that we added.... idk.
I guess the builder function can take whatever Impl it wants, do the conversion, and stick it in.
No reusing the format scratch though, unless we're gamers and we use a thread local arena (don't do this)
One thing we never considered is that when the nodeparams or text are the result of an expensive calculation, dividing cosmetics and layout means you need to put an if() in both places if you want to skip it cleanly.
For example in the current counter() the text formatting for "count" is skipped if show=false because it's written together with the layout. But the calculation of the color isn't.
What's the lesson here? I guess that it's cool to have the freedom to do it the way you want.
If doing it inline is hecking cool and valid, then it also kind of excuses the v_stack() behavior, since you can see it as a shortcut for add_node(VSTACK).place() which is normal.
At that point, you can harmonize is_clicked() and friends in an almost similar way, by moving those methods onto Parent, I think? Not sure how it'd work with slab_i's instead of Ids, but it's probably fine.
It'd still be either that or nest(), but that's ok, I think.
(remember that nest() can't return anything useful because it can't hold a reference to Ui)
Parent can be renamed to PlacedNode which I think is good. Both clicks and nesting are only available on placed nodes.
Ui::is_clicked(KEY) can have in its docs the fact that it returns false if the node hasn't been PLACED in the tree recently, and specify that you do that with the UiNode::place() or Ui::place(KEY) node.
I was wrong, it would be possible in theory to have a UiNode struct with the &Ui ref and click it, probably, but the thing is that it has to start from the same struct as place() which can't have the reference.
So what???
We could totally move the clicked Vecs and stuff into thread local. but should we? hard to say.
Otherwise, just don't have that immediate style is_clicked().
Some hypothetical apis for high detail input would return references to those vecs, which wouldn't be ok if it was in a thread_local. But it's probably not a big problem.
Forgetting to call place() is still a problem tbh for when you want to do everything inline (and neither nest nor check for clicks.)
It's fairly easy to warn for it, I think.
It's actually impossible to warn for, because the same thing happens when you simply build a node, but put the place() behind an if statement, which is a valid thing to do.
putting all those clicked functions into thread local state just because nest() needs to have no borrows feels very wrong.
I guess there's a solution, if we're okay with having TWO useless functions: place() still returns UiNode which implements is_clicked(), and the old as_parent() comes back, and you have to call it before nest().
As before, we can have either that, or two different flavors of place(), or both.
place_as_parent().nest(|| {})
place().is_clicked()
this is logically better, but place -> nest is probably more common than people insisting to use the awful immediate click style.
so maybe switching the names would be better.
since this is for losers anyway, why not force them to do this:
place().nest(|| {})
place().is_clicked(ui)
aka pass the Ui object back in as an argument.
Not really sure it will work.
Let's actually write something
- add() is now a method that creates the nodes but doesn't set the tree links
- parent() is now called place() and sets the tree links
This probably requires a rethinking of all the twin crap?
Not necessarily. But maybe.
add() can work mostly the same way.
- place() can keep a separate count, so that the 1st, 2nd, 3rd calls to place() place the 1st, 2nd, 3rd add()ed nodes.
- What if there's more place()s than add()s?
I guess yell a warning, and either:
- reset the last one
- do nothing
- spawn clones
- is_clicked() only gets the first one, as always.
(We could also forget this all-in-one-line thing.
```rust
ui.v_stack().nest(|| {
#[node_key] const SHOW: NodeKey;
ui.add_node(SHOW).params(params).text(text).place();
if ui.is_clicked(SHOW) {
self.count += 1;
};
});
```
It's still all close together. it's fine.
If we use callbacks, the is_clicked_part won't even matter.
But we did lose the ability to drop the key.)
Some things in Node should become optional, like parent. but we will use dummy values for now, I think.
ui.add(key).params(params) I guess? and maybe add_anon(params)? that's not very good, but I will think about it later.
MAJOR HAPPENING:
keys defined inside one of the closures are local!
[X] some kind of panic. it's probably the fact that now we do updates on nodes via these functions
ui.add(INCREASE)
.params(BUTTON)
.color(color)
.on_click(|| { self.count = 1 })
.static_text("Increase");
while before those updates went on a local Params value, and it didn't get to the real tree nodes if it didn't get added to the tree.
I guess we have to do this:
- when watching for params changes, don't push the need relayout/need cosmetic shit event, but just set a flag on the node
- when calling place(), see if the flag is on, and if yes, push
if the flag is set but it wasn't added, it stays set, I guess. dont see a problem with that.
[X] after that, make the keys optional
ui.add(PARAMS).key(KEY)
even doe that doesn't even make sense
or maybe it does thanks to the epic hashmap/slab datastructure?? you can change the front cheaply without moving the node itself. wow, that'd be kind of cool if it works.
we concluded that callbacks are probably ok. but it would be better to just not use them
place() could be done automatically by add(), and then if you do explicitly, you overwrite??
if yes, should make sure that no garbage remains from the first call. for example it could still remain as a child of the first parent.
anyway it sounds too confusing.
although it would totally solve the one line problem... not really, because then you'd need the explicit place() before nest(), which would be the same as as_parent().nest() but more confusing.
Ok, allora balza.
so after all this what happened? we added place() which is an ok way of giving the option to separate and harmonize the params/text part and the layout part
we didn't even fully finish that, because what happens if you place a node that doesn't exist?
but I guess we can do whatever there
from there, it was tempting to make place() do the job of the old as_parent() (return a referenceless struct that implements nest())
it would have worked good, but we got greedy and decided that, since now you do params and place in one line without a key, we wanted a nice way to do is_clicked inline as well.
and we got it, with the "instant callback" thing.
But we were even greedier than that and wanted it to be like this:
if ui.add(params).place().is_clicked() {
}
(no matter what, it will always be is_clicked() XOR nest(), because both break the chain. so you'd still have to give up on single line occasionally.)
there are 3 ways to do it:
- finally find a way to use nest() on a struct that holds a reference to the Ui (won't happen)
- make the click results available from thread local stuff.
- add another useless function and drop the reference there instead of in place()
like this:
if ui.add(params).place().is_clicked() {}
ui.add(params).place().as_parent().nest(|| {})
or with some dumb gimmick:
ui.add(params).place().ne().st(|| {})
there isn't really any downside to thread_local click results, apart for a vague feeling that thread_local = le bad, and being too lazy to do it.
And I guess that if I convert to callbacks anyway it will be a mess for nothing.
let's say that we do thread_local anyway.
all that's needed is to copy the result into thread_local after resolving them.
currently is_clicked does some twin resolution stuff. but we just delete that.
when it's chained, it should use a slab_i, not an id.
[don't\do\this] add the slab_i to the clicked_vec records and use those in the chained if_clicked
[don't\do\this] copy the results into thread_local
[don't\do\this] write the function
----------- not actually doing it for now. even without callbacks, there might be good ways to skip all that code that will work better if the effects aren't in the same line
Since we're typing up a storm anyway, what's the real dangers of thread local stuff?
assuming we enforce 1-per-thread or use a slab with id's, I think thead local click results would have zero dangers of any kind. you'd have to add the slab id to the referenceless struct, but wouldn't be a problem.
[X] The params diff stuff isn't good anymore now.
It should use a hash:
place() compares the hash of the new params with the stored last hash
(remove the flag thing and go back to pushing directly then)
(the string functions should still use the flags)
Just typing a bit about the incremental stuff even if it's probably not that important
- the idea is that even we have all that code in place to skip relayouts and rebuilds/rerenders, we still run the declare_ui() function and friends every frame, which builds strings and params and hashes them to catch changes.
ways of skipping:
literally behind an if
if changed {
ui.v_stack().nest(|| {
ui.place(INCREASE);
ui.add_anon(LABEL).text(self.count).place();
ui.place(DECREASE);
}
} else {
ui.assume_unchanged(subtree root key which in that example doesn't exist)
}
same, but hidden inside a closure
if_changed(base_state, || {
ui.v_stack().nest(|| {
ui.place(INCREASE);
ui.add_anon(LABEL).text(self.count).place();
ui.place(DECREASE);
}
})
fn if_changed(code: impl FnOnce()) {
if base_state.is_changed() {
code();
} else {
thread_local_assume_unchanged_somehow()
}
}
what does assume_unchanged even mean?
- after a certain point, don't clear the old tree links, don't let nodes get dropped by the frame count, etc
- stop doing that when another widget closure is called? in the sense that a tabbed view has base_state = tab index, it doesn't change itself if the index didn't change, but its assume_unchanged obviously shouldn't propagate to the stuff inside the tab.
except maybe if we do something wild with #track_caller, that's impossible to do automatically, I think.
-------------
For tabs and maybe scrolling, we might add a "hidden" state where nodes are still part of the tree but hidden.
But why?
I guess the advantage is that when you switch to another tab, you don't relayout everything?
Sounds both risky and kind of useless.
If we want to take that kind of risk, why not just the other relayout optimization where we keep track of the last size constraints and skip relayout based on that.
The only real reason to do this if it helps with deciding when to purge untouched nodes.
Which could be a good reason. But how would it look like?
Everything gets insta-purged unless it explicitly says that it's hiding.
------
Missing from partial relayouts:
- image rects
- special case for 1st frame, then push rects while placing
Code quality interlude:
- bring some stuff into separate files, and de-pub it. for example Changes
- rewrite those recursive_x functions to avoid splitting into stack and non-stack
- slab_idx type?
Right now when the mouse is sitting on a button, it keeps bumping up the last_hovered_t, which is very wasteful.
what it should do is: when it enters, set a last_hovered_t to float_max so that it remains max darkness. when it exits, set it for real, but only once.
Important: in non-declarative mode do you just invalidate parent/child indices and break everything, or what?
No, the basic principle is always the basic "when you remove a child from parent, also remove the parent from child, and viceversa".
In practice I dunno. what happens currently is that we don't do that, we just reset all children from a node whenever we readd it. so the children do stay in the timed-out state with possibly dangling parent links. but it doesnt matter because if they'd be readded, they would be insta cleared and reset.
idk what the invalidating situation is even supposed to be.
for explicit add_child/remove_child, just follow the glodden rule above.
-----------
a node could have a "reset subtree on disappear" which means that when it disappears it permaremoves (instead of hiding) itself + all children.
Remember that it only matters on nodes with state, ie. only NodeEnter.
Permaremoving could also happen when too many tree modifications happen?
However we currently don't count how deep the removed subtrees go.
Also, I'm not sure anymore that slabs in a rect are a good idea. Because most of the time, nodes get invalidated by just not being refreshed, right? so there's no moment where you can go "ah, I see that this node has been removed, let's remove its rect as well."
To make the rect be auto-invalidated in the same way, the rects would need to also have last_frame_touched (which the shader uses to skip), and most importantly the rects that DON'T get invalidated need to be bumped up.
So we're trading once-in-a-while full rect passes for constant bookkeeping every frame.
That method would be symmetrical with the TextAreas, which I guess would be nice. But the point there is that they're hella expensive to recreate. Rects are free.
So maybe we still want the plain Vec after all.
The thing with rect was
- those 3 ways of interacting with it
- one of the 3 ways is cringe: when you do a partial relayout after a node got added or removed, you invalidate all rects, basically, and you have to do a full node tree traverse to rebuild all rects.
That's not so different from doing a full relayout.
Performance vibes seem still in favor of the plain vec, because doing stuff every frame is the worst.
Complexity is honestly not too different, because the gpu arena is a mess of its own.
There's always the option to always do build-rects as a separate pass, which would simplify things a bit for the vec.
ok, so we're doing this:
[X] rip out the slab stuff from renderrect
[X] bring rect building into a separate function for simplicity
[X] - add partial relayouts
- add rect rebuilds
- add rect updates
to summarize:
we go into relayout() with:
- partial relayouts (atomic: size, stack)
- tree changes
- cosmetic rect updates
- "rebuild all rects"
we do this:
- if there are too many partial relayouts or tree changes, do a full relayout. (choose limits and weights)
- if there are any tree changes, the rects are all invalidated. set TREE_CHANGED = true.
- set REBUILD_ALL_RECTS = TREE_CHANGED || explicit rebuild_all_rects.
- do partial relayouts for all tree changes and partial relayouts.
if REBUILD_ALL_RECTS == false, also do partial rebuilds.
- if REBUILD_ALL_RECTS == false, do cosmetic rect updates.
- if REBUILD_ALL_RECTS == true, do a full rect build pass.
partial rebuilds aren't really a thing. it's more like partial updates. but that's retarded, I think.
For now I will do a full rect build pass all the time.
What now?
[X] Actual rect updates
[X] Skip rendering
let's get back to the famous 3 ways.
actually, just 2 ways for now:
if tree is invalidated, so full_rebuild_rects.
if not, do cosmetic updates.
that is all.
[X] ... looks like "filled" actually meant "not just the outline". lets add it back with a better name. "outline_only"?
-----------
- TREE CHANGED:
- partial relayout: (aka a child was added somewhere but nothing else changed)
- do a full rectbuild pass after
- full relayout:
- do the in-traverse rectbuilding (same as old version)
- TREE NOT CHANGED:
- both full and partial layout:
- do in-traverse rect updates
3 ways of building these stupid ass rects? for real for real?
new plan: upload the whole slab on the gpu and call it a day.
we have to build a custom slab doe:
- SOA: two parallel vecs for the part that goes on the gpu and the part that doesn't
- method to get a slice
- gpu-readable layout including the skip bit
- "real length" method to skip uploading
- maybe merge the "skip" concept from slab with our own "last frame touched" concept, at this point??
probably not.
- let's not worry about the mouse checks for now, because there should be good ways of optimizing that separately.
if anything, we'll just do a 3rd SOA vec with just the AABB. at that point it should be small enough linear scans are fast no matter the skipping situation.
linear scan on the render part is also fine, probably.
Just for reminder: at most times, the slab has a bunch of nodes that were left behind / not touched during recent declares, but not pruned yet, and ALSO some straight up empty slots in the normal slab sense, from when prune() was called.
Until I make up my mind about when to call prune(), the gpu should skip both types of "blocks".
!!!!!!
I forgot that many nodes don't have render rects, like the stupid ass vstacks.
if it was fully static, they could sit in a separate array, but that's kind of giving up muh simplicity. Also in theory you should be able to add or remove rects dynamically, so it doesn't even matter 2bh.
is it just over then?
probably.
the stuff with tree changed above could be simplified to this:
- TREE CHANGED:
- do a full rectbuild pass after
- TREE NOT CHANGED:
- do in-traverse rect updates
this is probably about the same tbh. not a big deal.
the waste is having to rebuild all rects. doing it in the same pass probably cambia poco o niente.
so... new new plan:
- implement the famous stuff to determine the list of needed relayouts / color/time data updates
- figure out the rest after, at this point. Idk kev.
the famous stuff:
keep track of how much RELAYOUTING and or RECT UPDATING will be needed
so, just keep track of which nodes got a:
- [X]removed/added child (=> relayout)
- [X] nodeparams.layout update (=> relayout)
- [X] text update (=> relayout)
- [X] nodeparams.rect update (=> cosmetic rect update)
- [X] timestamp update (last_click etc) (=> cosmetic rect update)
[X] separately, keep track of the famous "upwards constraint chains". immediately track relayout-related events as belonging to the top of the chain.
then, do this:
- TREE CHANGED:
- do partial layout.
- don't do cosmetic rect updates.
- do a full rectbuild pass after.
- TREE NOT CHANGED:
- do partial layout.
- do cosmetic rect updates.
[X] about those chains:
when pushing a parent, if it's FitContent, then push a Chain(id) on the chain stack.
when adding a new_node,
- if chainstack.top() == Chain(id), then set new_node.chain_root = id.
- if new_node == FitContent, don't push anything (keep the root there).
- if new_node != FitContent, push a Chain::Break(id) on the chain stack.
- if chainstack.top() == Break,
- if new_node == FitContent, push another Chain(new_id) on the stack (as normal).
- if new_node != FitContent, don't push anything.
when popping a parent, if it's the same as the last guy on the chain stack, then also pop from there.
(for both Chain and Break.)
and that's about it.
about detecting changed children:
the only place where we really know that the parent just ended is in the nest() function, where we don't really have access to the node.
The old children signature can be passed through the parent struct by value, but the new one?
I guess it's another stack or something?
Yeah.
when pushing a parent, push a new hash empty hasher.
when adding a child, add the hash into the latest hasher.
when popping a parent, do hasher.finish(), and compare it with the old signature.
... but here's the thing, how do I write it in the node to update the new old signature??
I dunno.
[X] actually, just use the linked list, I think. whenever you update the next_child or first_child fields, if it's different from what was already there, then the current parent has changed children. that's literally it.
[X] can't we also rewrite the chain shit without using thread local at all? lol
- if parent.relayout_chain_root == None:
- if self.params == FitContent, set self.relayout_chain_root = Some(self.i) // start chain
- else if self.params != FitContent, set self.relayout_chain_root = None // do nothing
- else if parent.relayout_chain_root == Some(i):
- if self.params == FitContent, set self.relayout_chain_root = Some(i) // continue chain
- if self.params != FitContent, set self.relayout_chain_root = None // break chain
it's literally that shrimple.
[X] i fratelli:
parent should keep a latest_child cursor.
I guess it can double as a last_child if we ever need backwards traversing of the linked list. but that won't happen.
... or maybe, we just always traverse it backwards, so 1st child and last_child are actually the same thing?
yes.
actually, everything's borken now.
maybe with how last_child is changing all the time, the check doesn't make sense anymore.
but maybe we can just check parents and siblings instead of that.
checking parents is dumb, I think that there has to be an old_first_child but it's not pushed in the same way as the movable "first child" that we're using now.
So maybe we're going back to before, but it's ok. maybe only the old one is needed.
Anyway, for the tree change checking, we decided that we have to use thread locals after all.
but maybe just for a final last_child check?
to be honest, that hashing stuff seems simpler right now.
the advantage of not doing it was that we could maybe skip some checks. But who cares. it's a lot of bookkeeping and thinking. I hate thinking.
todo: move all thread local functions to a module, so the function names don't look so dumb.
can keep "tree generations" to avoid doing overlapping partial layouts.
should think about this doe: if a partial rerender starts at node A, it considers as fixed the size of its immediate parent, and the result can depend on that size.
So, if A is relayouted (with all its branch), then a node B higher up than A is relayouted and sees A, it cannot skip it, because new starting size => different layout for A as well.
SO, we have to carry around the TREE DEPTH for all those guys, sort the array by ascending depth, and do it like that.
So it's always the reverse case (A gets relayouted after B, it sees that it's drowned in a sea of already relayouted nodes, it can rightly decide to skip.)
This also means that there's no need to check the generation at every step. Just once at the beginning.
- Maybe they actually come pre-sorted?? dunno, should check.
-----------
when to reset tree_changed?? after render? what if it's test mode/no render?
probably split it into need_relayout and need_rerender
-----------
when to call clicks.clear()? depends on a lot of things, I think. (how the outer api looks, if partial immediate mode is a thing, etc)
new plan: keep a plain record of inputs without hitting the rects, then hit the rect on demand when the user calls is_clicked etc.
then cache the hit, so that subsequent calls can skip clicks that are already attributed to other nodes.
just to be clear, this is what you'd want to call:
- is_clicked -> if there is at least one matching mouse_down event
- is_click_released -> if there is at least one matching mouse_up event
- is_double_clicked -> if there is at least one matching mouse_down event, and some flag is set in the node
- is_held -> there's only one possible held node: the one where the cursor is sitting at the end of the frame. if the cursor is sitting on a node and there was an un-upped down, set the held flag, and register the id somewhere globally. Then, reset the flag either if the cursor isn't sitting anymore, or on a hitting mouse_up.
- is_hovered -> this is the possibly heavy one. but still better this new way. Either keep a full history of mouse movement events, or just the position at the end of the frame. check hits. done.
- is_dragged -> same as is_held, but don't reset on mouse exit.
full_click_info(node)? -> hit all events and return them? do I have to use an iterator here? that'd be cringe.
full_click_info(overall)? -> kind of useless, right?
The new plan seems better especially because it allows to make everything clickable by default.
potentially use a RefCell so that is_clicked can take a non mutable reference, but can still write those cached values and shit.
Calling it a day for inputs for now, but the advanced side that gives StoredClicks could be expanded for sure.
-----------
however, the big problem is still that when twins are involved, the identity of nodes is not truly stable.
ui.text(&string1)
if flag {
ui.text(&string2)
}
ui.text(&string3)
&string3 will sometimes have twin_id = 2, sometimes twin_id = 3.
That's not a problem for static_text but it's definitely a problem for dynamic_text.
However, we could use that old tree-comparison thing to determine if there *could* have been a similar situation.
...not really though.
In the example, you don't see immediately that it's different. You only realize after the string3 call, and at that point, the reference is gone.
if you end up relying on hashing a lot, you can do still do a pointer equality check. If the pointers are different, it's fair to jump to relayout anyway without wasting time hashing.
...wait, does this cover all possible mistakes due to twins?
not really:
let mut string1 = String;
ui.text(&string1)
string1.push_str("asdfsdf") // mutate
if flag {
ui.text(&string1)
}
string1.push_str("asdfsdf") // mutate
ui.text(&string1)
string1.push_str("asdfsdf") // mutate
here, the string content is different, and the twins get messed up, AND the pointer never changes.
this is a pretty annoying and rare case, but I don't really know. Remember that the twins alias with stuff far away into the program.
If the user is nice enough, he could do this for us:
if flag {
ui.text(&string1)
}
ui.text(&string1)
ui.text(&string1)
ui.maybe_text(&string2, flag)
ui.text(&string3)
in this way, we could still advance n_twins in the maybe_text call. It's crazy to expect people to do this, of course. But what if...
})
the custom if guards the code with calls that set and remove a "advance n_twins but don't add the node" mode (in a thread_local).
Obviously this is insane.
Also, the same thing happens with for {}. If the loop iterates 10 times on frame n, and 2 times on frame n+1, then
it seems that the only way is to tell people to not use twins.
twins still have these advantages:
1) for anonymous v_stacks and similar. This splits in two:
- for tree diffing. You could just store the fact that it's a worthless v_stack, and ignore them in the diff.
- for not recreating the nodes every time. If we did, we'd have to do a purge step. But we might have to do that regardless? But probably not on every frame??
2) so that you don't just get a fat crash if you accidentally reuse a key.
Also, what about change detection on params??? hello????
Just kidding, who cares? just have an optional mode where updates get ignored by default.
In that mode, you can re-enable them on demand using a global refresh_all(), a global refresh_rects() (for theme), with dynamic_X() builder functions for the node, or whatever else.
this might be a good argument for keeping the basic text(), having it break when you enable performance mode, and telling people to switch to dyn_text.
--------
format_scratch thing seems like it would work.
if cosmic_text had its own method that takes an `impl Display` that might be better. Or it might be the same if they need to lay it out into a buffer and then scan it for newlines (likely)
--------
should try that new node_key!() approach soon that's meant to be used inside NodeParams::with_key()
like this:
let increase = BUTTON.key({
pub const COPE: NodeKey = NodeKey::new(Id(123123123), "cope2");
COPE
});
...probably impossible to let the const exit the block
----------
the two builder patterns skissue
realistically, only 3 solutions:
- keep aaa() (awful)
- go back to lifetimesoup hell
- pass text and image to add() as a different argument. there can be variations to this:
- single method, pass empty strings everywhere (awful)
- different named methods: awful
- something smart with tuples: add() takes an impl FullNodeParams, which has methods that return None if inapplicable,
and tuples like (NodeParams, &str), (NodeParams, &u8) implement the trait.
I think this makes it impossible to tell apart 'static refs from others.
- always pass a NodeParams and a NodeContent, most of the time it just happens to be NodeParams::nothing
- what about this:
-> Parent
self.ui.add_parent(¶ms).nest(|| { ...
-> UiNode -> UiNode -> UiNode -> Parent
self.ui.add(¶ms).text("Seethe").image("Cope").parent().nest(|| { ...
-> UiNode
self.ui.add(¶ms).image("seethe") // no nesting
it's not really common for things to have both content and nesting.
This is ok, I think.
Basically we keep aaa() but we call it parent(), and for Ui we make a add_parent() which is both add() and parent().
Looks pretty grim either way.
Maybe lifetime soup wasn't that bad?
What to actually do now?
- split Ui into nodes and everything else
- after that, look how nice NodeRef looks
- and then, finally rewrite update_node
- lifetime soup
- change params on refresh
- static stuff
- rename "defaults" to "params" everywhere
- remove anon_key!() and spam twins
- maybe move is_clicked and friends on the node ref
There was also a plan to put back node typing by making NodeParams typed as well.
--------
Change of plans: static_text doesn't mean "never changes". It means an actual &'static str.
This is because we can checking changes in &'static str's by just checking the pointer.
This can have false negatives, but that doesn't matter, we just relayout it that 0.1% of the time.
It doesn't work on static mut, but nobody uses that.
I don't think there's any way of having a special path for &'static, so it has to be a separate function: static_text()
dynamic_text() can take an argument that can mean "assume it changed", "assume it didn't change", or "Dunno, hash it and see". The ergonomics don't even have to be bad:
enum Changed {
Changed,
DidntChange,
HashItAndSee,
}
dynamic_text(text: &str, changed: Into<Changed>)
...and implement Into<Changed> for bool.
HOWEVER, this seems still worse than having three functions: "smart_text(changed: bool)", and a general purpose text() which hashes by default.
--------
Twins probably have some big problems with static text. It should probably be straight up disabled for them.
Example:
ui.static_text("A");
if condition {
ui.static_text("B");
}
ui.static_text("C");
imagine that condition goes from true to false. "C" is now the second twin. But the second twin has "B" from the last frame.
For twins, static text is a lie, because the calls end up into different nodes every time.
For non-twin siblings, it should be fine.
Note that nobody is forcing us to make the text() function rely on twinning. It could get an ID by hashing the text. But if the text is dynamic, it would spam new nodes every time, as well as new textareas. Not as big of a problem if we prune, I guess.
if we keep twinning, what about making the id be based on the tree position?
Of course, the REAL SOLUTION is to just give it a key.
Could this also be a problem with anonymous vstacks?
What if you get its size and use it for something?
That would be its size from last frame. But what if last frame it was a different twin?
(maybe just don't allow getting the size like that.)
----------
New plan: builder pattern (everyone likes the builder pattern)
- don't go ALL the way: still keep NodeParams for simplicity. As a second step, there' sstill space for builder functions for individual fields of Params. But it'd be Two Ways Of Doing The Same Thing
- just use builder functions for text, image, and starting a parent block.
- I don't think the key could pass through a builder function, because you need to know the ID immediately.
it could still go into Params though.
note that with the closure + builder function is probably impossible, because all builder funcitons are called on NodeRef. But I'm pretty sure that you need the whole Ui object for the closure.
This could change if Node keeps a slab index instead of a reference.
You'd still get borrow errors if you try to hold the NodeRefs, since they still have a ref to the ui System.
But it could still be a bit dangerous. ...NodeRefs are invalidated only when pruning... but I still don't know when I'm going to prune anything. I dunno.
------------
With the lifetime soup, people might want to do this:
// build the params in a function
let text_params = self.build_text_params();
ui.add_parent(VSSTACK77, &V_STACK, |ui| {
ui.add(TEXT77, text_params;
});
// the function does this
fn build_text_params(&self) -> NodeParams {
// allocate a string
let text = String::new()
// ... format it
return NodeParams {
// ...
text = &String
}
};
This would be impossible, because the allocation lasts only for the scope of build_text_params.
They'd have to allocate the string in the outer scope, and pass it in.
When trying to return NodeParams, you quickly see that you basically can't do it, because of the lifetime soup. So you quickly decide to build it in the same way.
Still, it's a bit limiting.
If the text was set with its own function, then it'd be separate from NodeParams, and you probably wouldn't have the temptation to create it anywhere alse than the correct place.
On the other side, it really would be better for Params to just cover everything.
What if you were formatting a string like pixel_info, but it also depended on the global language settings? wouldn't you build that in a separate function?
Yes, but if it was a function, you get out of it free with AsRef<str>.
.........
Sticking the key inside the params is totally doable, and might be good, especially for typing.
Could totally skip the lifetime soup by forcing text and image updates to go through some other method, probably on the noderef.
Probably shouldn't do that, though.
Also, aren't noderefs a bit odd to use with the closure syntax?
I added the twin N in the debug name, but maybe that was stupid to do it like that, because the same node can have different twin N every frame.
I guess in many cases it would stay constant and maybe it's still useful there.
--------
Didn't really find anything better than the macro or the manual add_parent/end_parent.
These were the options:
- Raii guard + global stack, no macros: very easy to fuck up if you ignore the must_use warning, aesthetics are bad. Even worse if the returned node isn't relevant in any other way
- Raii guard + global stack, with macro: the macro doesn't need to access the Ui struct, so it can be unhygienic. it could look like this:
ui.add(SLIDER_CONTAINER, &slider_container_params);
parent! {
ui.add(SLIDER_FILL, &slider_fill_params).set_size_x(Fixed(Pixels(value as u32)));
}
it's so similar to the old macro that it's not worth adding the globals.
- closures: actually works fine.
I just assume this will give some dumb borrow errors sooner or later. I don't really understand closures enough to tell.
ui.add(SLIDER_CONTAINER, &slider_container_params, |ui| {
ui.add(SLIDER_FILL, &slider_fill_params).set_size_x(Fixed(Pixels(value as u32)));
});
looks like it would work without globals as well, so maybe I'll try it and keep it around.
I will write in the totally-going-to-exist docs, but the idea is that closures just suck because of the incoprehensible gibberish that you get from the compiler if you use them wrong. Example:
self.ui.add_parent(PIXEL_PANEL2, &PANEL, |ui| {
self.ui.add(TEXT77, &TEXT);
});
for anyone but the most autistic language experts, the |ui| {} is just some random black magic that you do without thinking. So these kind of errors are mostly just gibberish.
Also, I have distinc memories of real borrow checker errors when trying to access game state from within Egui closures. Don't really remember though.
It is worth remembering that we're also doing anonymous keys with macros. Although it would also be ok to spam the same ANON_TEXT key and let it twin. Should probably just switch to that.
-------
Should write a recap of the plans for
- sleepy mode
- partial relayouts
- static'ed nodeparams, images and text
so I don't forget it
Also, TODO: is_dragged, is_clicked, get_ref etc should all access the last twin added. Nothing hard about it.
... the twin thing killed some of the performance of our clicked() approach, I think, because when calling is_clicked(SOME_KEY) we have to go a node.
Lmao, the twinned thing means that you have to put the is_dragged AFTER adding it, or it will refer to the last one.
That's pretty terrible.
This is a widget-only problem. In non-widget, it's kinda fine.
Either way, it's always been clear that if you ever need to refer to the nodes, you shouldn't reuse the key.
The last-twin behavior is fine, but it should give a big warning.
... this can be avoided by using the returned NodeRef for is_clicked etc.
-----------
If we still use macros for anon keys, then we can move the debug_text into the params. That way it's not const, and file! line! column! can go into it easily. Non-anon keys still have the debug text in the key, so this would mean having debug text on both the params and keys and using either. It wouldn't be a deal breaker.
-------------
This time for sure, it's decided, we split key and params.
To add back the type safety, we simply make typed versions of params. For example, TypedParams<Text> will straight up not have a Stack field.
Internally, it will implement a to_nodeparams() method, which gives a NodeParams with stack = None.
The add() function can be like this:
pub fn add<T: NodeType>(&mut self, key: TypedKey<T>, defaults: &TypedParams<T>)
with a single T, so it's enforced that you can add a TypedKey<Text> only with a TypedParams<Text>.
The two lines? we just write them.
I guess in the totally-going-to-exist docs, we show how you can you can usually avoid it.
-----------
I guess we're back to nodeparams inside nodekey, and builder methods on the key?
----------
it feels so bad to write those two useless lines!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
And typed keys are so useless now!!!!!!!!!!!!!!!!!!!!
non-const keys are still on the table, but I think it really sucks to not be able to do if ui.is_clicked(whatever button I want) wherever.
Also, the attribute doesn't work, so no debug names and shit.
There was some idea about putting the key inside the params, but it seems cringe.
There could be a 3rd type which is not a const, but holds a regular NodeParams and an &'static NodeKey.
-------
if the key is consts and the params are non-const, then they can't stay together, huh.
- the compromise that we have now is the following:
the params are split from the key, so that they can be non-const.
However, they're the DEFAULTS. you can't live edit them.
It's definitely a good idea to try to make this as clear as possible. Right now, the argument name is "defaults", but that's pretty much the only clue.
- should rename the NodeParams struct to NodeDefaults.
- should log a warning in debug if the user refreshes a node with different defaults.
I think we get some good stuff out of this, maybe.
We could also just stop being scared of immediate mode, and just allow live edits.
What if you change the theme? Don't you want everything to update immediately?
What if you change the *language*?
even assuming that the lifetime soup was fine, we still absolutely need a way to not go through 1MB of image data every frame "just in case it changed".
solution: just add a "changed" flag.
Then, add different builder methods:
with_image(image, changed) is explicit,
with_static_image(image) is the same as changed=false always,
with_dynamic_image(image) is the same as changed=true always,
and maybe with_image_hashed.
The entire key could have a changed flag. There was that idea about the node_key macro setting changed=false, and all builder methods setting changed=true. But with the widget creating thing, that wasn't correct anymore, because it would always pass through a builder method.
However, let's not forget that we can totally go back to const keys with params inside after this.
In the widget creating thing, it'd be weird to create a "superdefault" and then change it. But not THAT weird.
- when doing that widget thing, having keys as const is actually limiting.
For example, it would make sense to call add_slider with a Position param that changes the position_x here:
#[node_key(PANEL.size_y(Fill).size_x(Fixed(Frac(0.4))).color(Color::FLGR_RED).position_x(Start) )]
const SLIDER_FILL: NodeKey;
But you can't do that if it's a const.
(Even if it was a const, the syntax would be still wonky. Right now that add_widget function can't take any params. The free function version works perfectly, ofc.)
"editing the params for live changes" would go well together with non-const keys, conceptually.
Non-const keys is fucking annoying though.
You can always do
add!(ui, SLIDER_CONTAINER, {
ui.add(SLIDER_FILL).set_position_x(End));
});
or
add!(ui, SLIDER_CONTAINER.set_position_x(End)), {
ui.add(SLIDER_FILL);
});
but that's like a live thing.
I guess you can maybe use generics or macros or something to change it while it's const... that sucks though.
...starting from the obvious, aka that you should be able to add a node with non-const params and have it work, what's next?
i dunno.
Static type safety becomes all pointless, because you can re-add a key with completely different params.
Not really: if the idea is that all builder methods are on keys, then maybe we can just not implement those builder methods on the wrongly typed keys? dunno, should try it.
At that point, let's do non-const keys as well. If it was a normal macro instead of an attribute macro, it would work automatically. But for now let's keep the attribute macro for the aesthetics and for having access to the ident to stick into the debug_text.
- non-const keys
- lifetimes in params
- move builder stuff to key
- refresh
The attribute macro can't make a non const key. Lol.
Separating key and params completely? That would work nicely with constness, debug name, etc, but what about the type safety?
You could always have logged warning safety, I guess. Considering that the "compile time" safety was so janky anyway, maybe that's fine.
New plan:
- split key and params
- lifetimes in params
- trim_lifetimes()
- refresh changes the node
- no macros would still be a great thing.
- consider going back to editing the params for "live changes"
cons:
doesn't work well with consts (probably not real)
doesn't work well with images possibly (can't have &'static [u8])
there's just too much crap to stick in there. attributes? image transformations?
nodeparams would get huge, and in this scenario, nodekey would hold a full nodeparams, not a static ref.
pros:
having just nodekey instead of nodeparams would be simpler.
noderef + set_position() style is better for hypotetical retained mode mode.
BUT CONSIDER THE FOLLOWING: that would allow retarded things like turning a stack into a non-stack.
It would probably work fine automatically, but it's still something that nobody should be doing.
isn't there any inverse approach here?
Like instead of putting everything in the key, doing everything with set_x() function calls?
Not really. But if it was retained mode, the problem would kind of disappear either way.
Maybe it's really time for some retained mode.
Even if we keep both nodeparams and noderefs + set_x()'s, we could still make updating the nodeparam work.
That would be even less clear, I think.
Should really see if the "too much crap" really makes sense or not. It's probably ok. With maybe some lifetime trash, it could be (for example) a &Attributes, and you either stick in some static default attributes, or you create a new one for the updates.
-- container widgets like a tabbed panel. How does it work? is it a custom macro?
Maybe the widget is just the tabs, and you do an if based on the state of the tabs to draw whatever.
But probably the idea of a container widget still makes sense? Does any other framework do this?
Images are still missing a lot of stuff:
- the allocator thing needs a hashmap, otherwise re-adding each icon would reallocate its image, I think.
what's the key? hash of the whole image data? Ideally, I wouldn't want to go through it every time to hash it.
...maybe something with line!() and column!()??
- the shader sucks
- how it works with nodes is not clear either, still. The old idea wasn't bad, honestly, I think that the only flaw is that if you have a big rect with a small non-stretched image at the center, you can't detect a click on the image only.
images and such...
need access to the queue to load textures to the gpu
there's no way Ui could keep a ref to the queue.
there's no way it could be passed as argument, I don't think.
so the only way is to wait for prepare().
when loading
-- what a nothingburger that was, huh. Since we have to decode the image, we still allocate for it. So nobody cares if we make that allocation last until the next prepare().
Partial relayouts:
- keep track of fitcontent-chains.
- every frame, populate a list of all the relayout events that happened, like if someone calls set_text(), or maybe if some node's children change.
- convert the rects vec into a DenseSlotmap.
- if there's a lot of events, be eager to just give up and relayout everything. In that case, just clear the slotmap, and do as we do now (push a new one every time.)
- but also, on each push, store the slotmap key into the node.
- if the events are few, do a partial relayout. instead of pushing new rects, use the stored keys to change the rects in place.
It's possible that this only works for cases where you want to relayout but you're sure that no rects disappeared, like if the "events" are just set_text().
Otherwise, you'd have to go through all the rects anyway to remove the old ones.
There'd definitely be some overhead: pushing on the slotmap is slower, storing the key, keeping track of the chains.....
But the case where just a set_text() is called and nothing else happens is probably very common.
Wow! if we restrict it to the case where it's the same nodes and same rects, you could do it without the slotmap. Just use an index. Kinda wild.
Actually, when you add/refresh nodes, it's super easy to know if you're adding a new rect, and also pretty easy if you're removing rects: just look at the current parent's children, and see if they all get refreshed.
That's kinda advanced, thoughever.
But in that case, you could totally do the equivalent of build_rect right there.
Some notes on the typed keys thing: The original compile_error!() idea was wrong, but there might be ways around it.
Maybe, a validate() const fn which returns its input untouched, but calls some functions and panics if they go wrong.
Also, its probably time to put the textareas into the slab/slotmap.
We kinda need FitContentOrMinimum, I think.
Currently leaving behind some boring stuff:
- the rest of the text sizing cases below
- the advanced stack cases like SpaceEvenly or whatever
- the entire typed keys thing.
Also, what about this:
- Size::AspectRatio. If both X and Y are AspectRatio, figure something out.
This stupid ass text thing:
fn determine_text_size(&mut self, text: Text, proposed_size: Xy<f32>) -> Xy<f32>
Size can be FitContent on X, Y, or both.
-- if Y is FitContent (=> fixed sizeX), layout the text in a box with infinite Y and finite X. It will overflow down. Trim the Y based on how far it gets. This is easy.
-- if X is FitContent (=> fixed sizeY), having infinite X means that it never wraps, so it's always one line. So, we need a distinction.
- if the text is short, it's correct that it doesn't wrap. So just layout the text in a box with infinite X and trim it, aka the same thing.
- if the text is long, then we need to figure out how many lines we can fit into the finite Y space that we have, and call that "n_lines".
Hopefully this is easy to do, at least approximately.
Then, we do the classic "layout the text in a box with infinite X", and measure the "total_width".
Then, we divide that by the number of lines, and add some safety space, and call the result "line_width".
Then, we layout the text in a box with "line_width" finite X, and the proposed sizeY.
At that point, it's shirley guaranteed to be a pretty good fit.
-- if both are FitContent, we can do whatever we want.
The simplest thing is to layout with the proposed sizeX and infinite height (same as the 1st case).
Then, if the resulting aspect ratio seems wonky, we can adjust the width and relayout.
Since it's all heuristics, we could also try to estimate the aspect ratio ahead of time instead of layouting it and seeing.
We could totally go back to the non-enum Params, do the "both children and content" stuff, and still do typed keys. We could have generic keys as well, if we really want, but it'd be the same.
The real skissue is that you really need to do the same for NodeParams for it to be consistent.
Mainly so that the builder pattern functions make sense.
If we went back to thay "builder pattern on keys instead of the params" thing, that would be solved, but meh.
Even then, it would still be a mess. Are the generic TEXT, BUTTON etc typed or not? can you create a typed one from an untyped one, or vice versa? Isn't that kind of complicated to keep track of?
(That's assuming that we want both typed and untyped, which we do.)
I still haven't thought too much if all this is Rust's fault or not, but it probably is.
All considered, maybe it's fine to have typed keys but not typed Params.
In this case, you'd just have a PhantomData marker on the key.
The node_key! macro could do a compile-time validation and tell you that trying to create a TypedKey<Stack> with a Params that has Stack=None, TextContent=Some, etc, is wrong.
After all, it'd be a honest to god compile_error!(), so with enough effort it could be an even better experience than the standard type system errors.
This gives infinite granularity too: you can have TypedKey<Text>, TypedKey<Image>, TypedKey<Content> which allows both text and image, ...
(there would be traits for each individual operation, like get_text, get_image, adding children, etc, and they would be implemented only for the correct TypedKey<Something>'s).
You just have to type it out.
If this is the way, we can totally half ass the validation and improve it later.
-------
Wow! we did it! so cool... now the keys have a "subtype" (a generic NodeType). so epic.
However... it's a whole ton of code. And what about that guy who wanted to have both text and an image on his button? (we'd still have textures and NinePathRects parallel to the text, but maybe he wants a legit well-scaled image.)
I wouldn't go back to the enum-but-no-generics thing. But maybe it would still make sense to go for the max flexibility, nothing is an enum, every node can have as many children and as many contents as it wants. (well, one of each type).
For that, just drop everything and go back to master, and figure out the part where FitContent fits to both the children and the contents (all of them!) when they're all there.
If the text is parallel to a child, we first propose the whole proposed area to it, then it layouts itself (parall. to a child choosing its size), and then we choose our real size, possibly shrinking to it.
Then in place_children() we can place the text, possibly.
This is clear enough, it's just about whether to make it that any node can have text (or any number of contents?) or if "having text" means "having a Text child".
The fact that a node could be "just text" means that Node and NodeParams need to be enums. It sucks both in Rust and in general, I think.
On the other hand, every node having a slot for every kind of content sounds like a lot.
Important thing: background textures and NinePathRects are not like text or images, they are like the Rect's color.
Once that's out of the way, maybe it's okay to say that if you have a TextContent or a ImageContent, then you can't have any children.
Still, I think the enum would look terrible. The builder pattern stuff doesn't work well with enums either, I think.
Is it really that hard to have content and children????????
Is it really that hard to use an enum????????
---
Actually, I don't think it will work...
probably content nodes should be a separate type, and its Size should always be FitChildren (or rather FitContent).
Because otherwise you'd need to decide where to place the content within the rect, which would mean the same logic for placing children. So it might as well be a children.
That's completely wrong though, just think of a non-single-label text box.
Also, shouldn't you have images between walls of text?????
Maybe that's going too far.
FitChildren should also consider the content, I guess. Putting a repeating/cutoff texture on a button is a different thing, I think. If you have an ImageContent, that means you want the whole image.
Godot did have that NinePathRect thing.
Maybe there can be TextureContent and NinePathRectContent alongside the other stuff.
What's missing now THOUGH?
- in layout:
- actually fit to text
- arrange modes for stacks: at least Center
- "align" for stacks, aka use the child's Position for the cross axis
- squares for custom rendering (for the color picker and the overviews)
- images and icons
- scroll areas??? (for the command line thing)
Holy moly! isn't it retarded to have a padding of Frac, as in Frac of the whole screen?? it needs to be frac of the parent, I think.
[ ] ~~~~~~~~~~EPIC IDEA:~~~~~~~~~~~~~
in sleepy mode, the ui pretends to go to sleep, but in debug mode, it still runs everything except rerender. If it sees that something would have changed, it can yell at you for making a mistake in the wake up calls.
~~~~
since we can now spam the same key many times, what if BUTTON, TEXT, and friends were keys? so the user doesn't have to know the distinction about keys or params.
maybe this means that the key needs to have the params in the struct instead of the static reference thing, but maybe that's okay since the trace/early thing got scrapped.
it probably doesn't even mean that tbqdesu.
because of the already well know fact that all the tree references get cleaned, we don-t actually care about the ABA problem or whatever.
so, should be using one of those halfass slotmaps with just the empty slots and the linked list.
what if retained mode? it's the same. whenever you remove a child, you also go to the child and remove the parent_i.
This is kind of true in general and kind of a truth nuke. is the ABA problem a meme? maybe it's not reasonable to assume all links are always bidirectional?
In this tree case, the links ARE bidirectional, and you kind of need to clear them both ways either way, so it sounds like it's always a meme.
-- in games, if makes sense to say that "entity x is following y" is a single-directional link. I guess that's the usecase for ABA resistant things.
(If I could get a hashmap that never invalidates indices when inserting, and lets me keep indices into it, it would be a lot better than all this crap, honestly.)
("never invalidates indices when inserting" mostly just means "no reallocation", I think.)
NEXT:
- convert stuff to use slotkeys instead of Ids
basically everything: all parent and children ids in nodes and in stacks
- after most nodes.get()s disappear, undo the partial borrow substruct, at that point self.nodes.nodes[slotmap] can become self.nodes[slotmap] which I think is nice
- finally do the children linked list thing
- then don't dip into the node slotmap when refreshing
- change text_areas into something that deletes. at that point, maybe remove the last_frame_checked hacked into glyphon.
- convert clicked_id and friends to slotkeys as well??
- use the ease of self.nodes[slotmap] to actually work on layout() and make it good
*** what was the point of pruning again???
it allows build_buffers becomes a linear run instead of a tree traversal.
but you're doing an extra linear run on the fronts (hashmap) to turn a node (slotmap) tree traversal into a node (slotmap) linear run. with slotmap, it's almost surely not worth it.
other pros'n'cons:
- tree traversal puts the rects in the correct z order automatically (good for resolving clicks)
- pruning + linear run also has the advantage that memory doesn't get filled forever
- build buffers could potentially be merged with layout, in which case it would be a *free* tree traversal.
- even if it's useless, prune() can be kept around and ran every 10 seconds or so to make sure memory usage stays chill.
..... but if everything works with a tree traversal, isn't last_frame_touched just completely useless?
no, it's still needed on text_areas. those don't work with a tree traversal, so obviously you need it.
I think text areas can kinda stay there and chill, like they do now. maybe they can have a separate prune() that runs even less often.
- last_frame_touched is also needed on nodefronts to distinguish twins.
ARRAYS
the big new idea is:
- a hashmap of "node fronts/slim nodes", with the bare minimum for just the most common case (refresh and do nothing):
- last parent and last prev child. if it's different, reach into the fat node and update it
- maybe some hashes of text or other content (the code to actually use this might be weird)
- LAST_FRAME_TOUCHED? isn't this needed on the real thing as well?
it wouldn't be if we did the prune pass on the slim nodes. that doesn't invalidate the parent/prev child links, because we're not retarded and we use a slotmap. so maybe that's the way.
the prune pass has to keep doing deletions on the slotmap, but that's not a every-frame occurrence, so the performance gains should still be there in the important case.
- the slotmap index, obviously
- and then a vec or slotmap of fat nodes with all the stuff.
the benefit was supposed to be that tree traversals (layout) is a lot nicer to do on a vec/slotmap instead of a hashmap with all the borrowing garbage and needless extra accesses.
if the fat nodes are an already-pruned hopslotmap, rects can just be recreated during a linear pass, maybe.
actually, creating the vec during a tree traversal pass was useful for putting them in the correct z order automatically.
maybe you can just do that during layout??
textareas can be a secondary slotmap thing
LEGACY OF THE TRACE/EARLY THING
basically it was working fine by itself but it was incompatible with the twin thing.
when NOT doing the trace thing, ui.add can immediately return the real-possibly-twinned id, so chained_set can just go straight to the right node without having to think about twinning,
With the trace, this code
for i in 0..15 {
// shortened as text!(ui, "sneed");
ui.add(TEXT_KEY).set_text("sneed");
}
results in these calls:
ui.push_to_trace(TEXT_KEY) // not twinned
ui.select(TEXT_KEY).set_text("sneed") // somehow has to guess twinning
ui.push_to_trace(TEXT_KEY) // not twinned
ui.select(TEXT_KEY).set_text("sneed") // somehow has to guess twinning
...
Also, what if twinning interacts with many different early calls??
ui.push_to_trace(TEXT_KEY)
ui.select(TEXT_KEY).set_text("sneed")
ui.select(TEXT_KEY).set_color(Red)
ui.push_to_trace(TEXT_KEY)
ui.select(TEXT_KEY).set_text("sneed2")
ui.select(TEXT_KEY).set_color(Blue)
...
It's true that twinning is a very very limited thing. Maybe it's possible to guess correctly every time.
But it would definitely be a mess.
So there are some possible things to do in the future:
- bring it back fully, figure out exactly the ways in which twinning can fuck it up, and make it guess correctly in those cases
- bring it back only for the kinds of nodes that can't be early-modified. there might be none of them, honestly.
- make all updates go through a staging mechanism similar to the trace thing, instead of being early accesses. I really don't want to do this, but who knows, maybe it would be good.
For now, it seems wise to keep it simple and just give up on that optimization.
removing the trace.
- just rip it all out
- chained method memes return the twinned id, so you immediately go to the right one without issues
vorrei che:
- translation sia in unità di pixel dell'immagine, non dello schermo (ci si evita un *scale nella parte dello zoom?)
- non ci sia da fare l'inversione della y in 60000000000 posti diversi
coordinates:
MOUSE COORDS:
0 --------> 1200_f32
0
|
|
V
800_f32
who decided this? Winit.
PIXEL COORDS (es. get_pixel()):
800_usize
^
|
|
0
0 --------> 800_usize
Who decided this? I don't know, maybe wgpu's write_texture.
SHADER TEX COORDS OR SOMETHING:
1.0_f32
^
|
|
0
0 -----> 1.0_f32
who decided this? Wgpu.
Weird aspect stuff going on.
I thought you'd just invert the mouse coord y once and mvoe on. but apparently not.
filtered input:
- for mouse, it sucks but there's no other way: when the winit event drops the user can't know if it's gonna be absorbed on the next ui frame or not.
is some form of "last frame" possible? I don't think so.
-- update: just run through the rects on every event. who cares?
- for keyboard, currently the keyboard event sees if there currently are any focused nodes. that means last frame. so it's fine to do the original absorb or return thing. I think it's the right way.
- coral
- buffers:
glyphon uses only "layout_runs", and "shape_until_scroll", "new", "set_text", "set_size" in the example
blue uses only "shape_until_scroll", "hit", and "cursor_pos_from_byte_offset"
- round all f32's to whole pixels
what to do AFTER figuring out layout:
- remove duplicated fields from node, and write straight into the rectangle instead
- separate the id -> i map and the actual vecs
- change tree links from Id's to i's
- turn usizes into u16's
- put a XY thing in rect and simplify the things
- do the epic linked list thing
I think that the layout pass needs some kind of Rect, maybe just min sizes or something, dunno.
But it still makes it kind of hard to separate the rects into a component array, because that would be for visible rectangles only.
So that's on hold for now.
- bring back anonymous columns. serve comunque specificare il layout.
- something about tree-position ids.
- the whole id system is easy to "use wrong", but it still seems better than the alternatives.
- add debug checks/warnings about reused ids (on add(), see if last_frame_touched is already == current frame)
- components:
- figure out the trait crap
- figure out subtrees, sub-ids, etc
- figure out "auto-interact" and "auto update"
- ...
- todo: id's in components are probably broken.
- resizing seems a big choppy.
- a macro or something for watching changed values might still be nice. think about it
- muh performance:
- don't build_buffers/render
- don't layout
- in the declarative part, don't even call add(), but just get a tree signature/hash. if it's unchanged, don't do anything at all.
then what? rerun it for real, or go through the tree "signature"? this will probably mean rediscovering Xilem and tree diffs.
--- before thinking about this, understand if both "completely idle" performance, "relayouting a lot, because of resize or similar", or in-betweens are relevant.
- did I force myself to a single click/event per frame? do I actually need messages like iced?
- what if setting text/layouts/colors was done with retained style calls (but spammed every frame) instead of through the key?
- "should the nodes hold any data" question
- dragged size/position? likely should, but it fits in the Node Size enum.
- scroll position? very likely should
- iced thing seemed cool. stuff below unrelated.
- key works as normal.
- add a ComposedWidgetState field to the main state
- in the builder code, run
ComposedWidget::add(&mut ui, COMPOSER_WIDGET_KEY, &mut composed_widget_state).
(or add!(ui, &mut composed_widget_state, ); maybe this is good to enable them to also be containers?)
(or add!(ui, &mut composed_widget_state, {}); maybe this is good to enable them to also be containers?)
in the function, it will composed_widget_state (instead of self.count ecc) to decide which rects and text to add and their layout/color/other basic stuff.
it will not draw any weird shit that the ui doesn't understand, like lines or color pickers.
if it wants to draw weird shit, it can store the wgpu state in composed_widget_state, and implement prepare() and render() like everyone else.
- run ComposedWidget::update(&mut ui, COMPOSER_WIDGET_KEY, &mut composed_widget_state).
internally it will do basic stuff like ui.is_clicked with all its sub-keys.
it updates composed_widget_state. as always, it could have the option to modify its nodes directly, but maybe it shouldn't.
(- would "subkey" ids collide between different instances of the same ComposedWidget? yes.
all subkey ids would need to be combined with the top-level ComposedWidget id.)
TODO
- good layout
- think about text and stuff.
- user owned text buffers?
- copying strings into it all the time? allocating for those strings?
- dragging things
- move the gets from is_clicked to the full tree resolve_input pass
- rebuild less and test weird removes/reinserts
- stop rendering when everything is still
- occasionally prune some stuff from the hashmap?
- animations...
- dynamic textareas?
NOTES
- right now idle cpu is kinda high, but it's not because of anything I do in update(). it's just because it's rendering a lot.
- is_clicked is kinda bad at expressing right clicks, ctrl+clicks etc, but so is the cl*sure approach. (also just_clicked?)
- the full tree resolve_input pass has to traverse through potentially a lot of non-clickable non-hoverable crap to get to the good ones. maybe keep a separate list of the clickable ids and just loop that. -- actually, only the non-opaque crap can be skipped, but it's probably not a lot. or potentially the non-overlapping non-interactable crap, but it's probably not worth it to keep track of that.
- or maybe it's possible to interleave it with the layout.
- since the builder code does just building and no effects, maybe it's not as cancer to use closures instead of macros for the pops() there.
- but maybe try the stored-tree approach like xilem.
⁃ many hashmaps for different sized types (don’t actually do this)
⁃ use rect_id to pass colors directly from nodekey to rect, without storing them inside node. Hover callbacks also have access to the nodekey (but not the resolve_input pass…)
⁃ always clear children in the builder pass!!!!!
- if I use the with_x builder functions for both building and updating, the with_update ends up everywhere.
- text content -- done, the layout part can wait.
- per-widget state + mutable text? -- done, but it's cringe. decide how excatly it would look to have the per-widget state in app state, and make sure it's easy to do the same with cosmictext buffers
- autogenerated ids
- anonymous columns/spacers (path ids? or fully random ids?)
- think about the id system
- user accidentally reusing them
- are the "anonymous ids" actually called in the right place?
- line!/column! can collide easily across files
- normal hash collisions
makepad does the following:
- get the hash from the identifier. doesn't look very good.
- ignore hash collisions.