Skip to content

Commit 2b254d3

Browse files
committed
Views: containers size to their content, and fixes for plain Dart apps
- Row and Column containers report the size their visible subviews need as their intrinsic size, so nested rows and columns no longer collapse to zero without a preferred size. Measured once per layout pass. - macOS: a container created under ARC is handed over autoreleased instead of being freed on return (View() crashed in the Dart build). - macOS: intrinsic sizes include the alignment insets, so buttons are not clipped in a process linked against an older SDK. - macOS: destroying a view that holds the focused field no longer emits ViewBlurredEvent into the tree being torn down (crashed at exit). - Application::Quit() on a loop that is not Run()'s own emits ApplicationExitingEvent on Windows and Linux too, and ends the process on Linux, as documented.
1 parent 25ad985 commit 2b254d3

11 files changed

Lines changed: 266 additions & 25 deletions

File tree

‎src/application.h‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -184,9 +184,10 @@ class Application : public EventEmitter<ApplicationEvent> {
184184
* are emitted.
185185
*
186186
* When the loop is not Run()'s own (a host such as a Flutter runner owns
187-
* it), there is nothing to return to and the process ends instead; on
188-
* macOS it goes through -[NSApplication terminate:], which exits with
189-
* status 0 whatever @p exit_code is.
187+
* it), there is nothing to return to and the process ends instead, after
188+
* ApplicationExitingEvent: on macOS through -[NSApplication terminate:],
189+
* which exits with status 0 whatever @p exit_code is; on Linux with
190+
* @p exit_code; on Windows the host's loop gets WM_QUIT and ends it.
190191
*
191192
* A quit the user or the system starts outside Quit() (Cmd+Q, logout) also
192193
* emits ApplicationQuitRequestedEvent. On macOS it cannot be vetoed and

‎src/platform/linux/application_linux.cpp‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,20 @@ class Application::Impl {
9898
return status != 0 ? status : app_->exit_code_;
9999
}
100100

101-
void Quit(int exit_code) { g_application_quit(G_APPLICATION(gtk_app_)); }
101+
void Quit(int exit_code) {
102+
if (app_->running_) {
103+
// Our Run() owns the loop: stop it so Run() returns the exit code.
104+
g_application_quit(G_APPLICATION(gtk_app_));
105+
return;
106+
}
107+
108+
// Someone else runs the loop (a Flutter runner, a host pumping it by
109+
// hand): there is nothing to return to, so end the process, as macOS
110+
// does through -terminate:.
111+
ApplicationExitingEvent event(exit_code);
112+
app_->Emit(event);
113+
std::exit(exit_code);
114+
}
102115

103116
// gtk_application_add_window() and g_application_hold() only take effect on a
104117
// registered application, and g_application_run() registers too late for that.

‎src/platform/macos/view_macos.mm‎

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ - (BOOL)isFlipped {
2222

2323
namespace {
2424

25+
// Non-zero while a view takes its native control out of the window on the way
26+
// to destruction. Removing a focused field (or a container holding one) ends
27+
// editing, and the blur that follows must not reach listeners: they may run
28+
// while the rest of the tree is going away, as at process exit.
29+
int g_views_being_destroyed = 0;
30+
2531
// The bridge is the delegate only while the view listens (HookControl), so a
2632
// field that nobody listens to reports nothing.
2733
NativeApiViewBridge* ListeningBridge(NSTextField* field) {
@@ -35,7 +41,7 @@ - (BOOL)isFlipped {
3541

3642
void ReportFocus(NSTextField* field, bool focused) {
3743
NativeApiViewBridge* bridge = ListeningBridge(field);
38-
if (!bridge) {
44+
if (!bridge || g_views_being_destroyed > 0) {
3945
return;
4046
}
4147
if (focused) {
@@ -212,7 +218,9 @@ - (BOOL)control:(NSControl*)control
212218

213219
View::Impl::~Impl() {
214220
if (owned && platform && platform->view) {
221+
++g_views_being_destroyed;
215222
[platform->view removeFromSuperview];
223+
--g_views_being_destroyed;
216224
}
217225
platform.reset();
218226
native = nullptr;
@@ -221,10 +229,14 @@ - (BOOL)control:(NSControl*)control
221229
void* View::Impl::CreateNativeContainer() {
222230
NativeApiContainerView* view = [[NativeApiContainerView alloc] initWithFrame:NSZeroRect];
223231
view.autoresizingMask = NSViewNotSizable;
224-
#if !__has_feature(objc_arc)
232+
// Handed over autoreleased, so it lives until Platform takes its own
233+
// reference. Under ARC a plain __bridge return would free it on the way out.
234+
#if __has_feature(objc_arc)
235+
return (void*)CFAutorelease((__bridge_retained CFTypeRef)view);
236+
#else
225237
[view autorelease];
226-
#endif
227238
return (__bridge void*)view;
239+
#endif
228240
}
229241

230242
void View::Impl::SetNativeFrame(Rectangle frame) {
@@ -264,8 +276,20 @@ - (BOOL)control:(NSControl*)control
264276
return Size{0, 0};
265277
}
266278
NSSize size = view.intrinsicContentSize;
279+
// That is the size of the alignment rect; a frame also covers the alignment
280+
// insets (a push button's shadow and bezel margin). A process linked against
281+
// an older SDK has such insets on current macOS, and without them the title
282+
// would be clipped.
283+
const NSEdgeInsets insets = view.alignmentRectInsets;
284+
if (size.width > 0) {
285+
size.width += insets.left + insets.right;
286+
}
287+
if (size.height > 0) {
288+
size.height += insets.top + insets.bottom;
289+
}
267290
if ([view isKindOfClass:[NSControl class]]) {
268291
// Labels and buttons know their width; an editable field reports none.
292+
// sizeThatFits: already answers in frame terms.
269293
NSSize fitting = [(NSControl*)view sizeThatFits:NSMakeSize(CGFLOAT_MAX, CGFLOAT_MAX)];
270294
if (size.width < 0) {
271295
size.width = [view isKindOfClass:[NSTextField class]] && ((NSTextField*)view).isEditable

‎src/platform/windows/application_windows.cpp‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,16 @@ class Application::Impl {
196196
return exit_code;
197197
}
198198

199-
void Quit(int exit_code) { PostQuitMessage(exit_code); }
199+
void Quit(int exit_code) {
200+
if (!app_->running_) {
201+
// Someone else runs the loop (a Flutter runner, a host pumping it by
202+
// hand): Run() will not return to emit this, and the host ends the
203+
// process on WM_QUIT.
204+
ApplicationExitingEvent event(exit_code);
205+
app_->Emit(event);
206+
}
207+
PostQuitMessage(exit_code);
208+
}
200209

201210
bool SetIcon(const std::string& icon_path) {
202211
if (icon_path.empty()) {

‎src/view.cpp‎

Lines changed: 60 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,35 @@
11
#include "view.h"
22

33
#include <algorithm>
4+
#include <cstdint>
45

56
#include "view_impl.h"
67
#include "view_layout.h"
78

89
namespace nativeapi {
910

11+
namespace {
12+
13+
// Layout passes in progress on this thread, and a number per pass. Within one
14+
// pass nothing changes size, so IntrinsicSize() measures each view once; a
15+
// container's size depends on its whole subtree and would otherwise be
16+
// measured again at every level above it.
17+
thread_local int g_layout_depth = 0;
18+
thread_local uint64_t g_layout_pass = 0;
19+
20+
class LayoutPass {
21+
public:
22+
LayoutPass() {
23+
++g_layout_depth;
24+
++g_layout_pass;
25+
}
26+
~LayoutPass() { --g_layout_depth; }
27+
LayoutPass(const LayoutPass&) = delete;
28+
LayoutPass& operator=(const LayoutPass&) = delete;
29+
};
30+
31+
} // namespace
32+
1033
// ---------------------------------------------------------------------------
1134
// Impl: the shared half
1235
// ---------------------------------------------------------------------------
@@ -20,6 +43,7 @@ View* View::Impl::Root() const {
2043
}
2144

2245
void View::Impl::RelayoutTree() {
46+
LayoutPass pass;
2347
Root()->pimpl_->Relayout();
2448
}
2549

@@ -28,14 +52,47 @@ void View::Impl::InvalidateIntrinsicSize() {
2852
}
2953

3054
void View::Impl::OnNativeResized() {
55+
LayoutPass pass;
3156
Relayout();
3257
}
3358

59+
Size View::Impl::IntrinsicSize() const {
60+
if (g_layout_depth > 0 && measured_pass == g_layout_pass) {
61+
return measured;
62+
}
63+
Size size;
64+
if (layout == ViewLayout::Absolute || subviews.empty()) {
65+
size = GetNativeIntrinsicSize();
66+
} else {
67+
std::vector<LayoutChild> children;
68+
children.reserve(subviews.size());
69+
for (const auto& subview : subviews) {
70+
children.push_back(subview->pimpl_->AsLayoutChild());
71+
}
72+
size = ComputeStackContentSize(layout, padding, spacing, children);
73+
}
74+
if (g_layout_depth > 0) {
75+
measured = size;
76+
measured_pass = g_layout_pass;
77+
}
78+
return size;
79+
}
80+
81+
LayoutChild View::Impl::AsLayoutChild() const {
82+
LayoutChild entry;
83+
entry.visible = visible;
84+
entry.preferred = preferred_size;
85+
entry.intrinsic = visible ? IntrinsicSize() : Size{0.0, 0.0};
86+
entry.flex = flex;
87+
entry.alignment = alignment;
88+
return entry;
89+
}
90+
3491
Rectangle View::Impl::AbsoluteFrame() const {
3592
if (has_frame) {
3693
return frame;
3794
}
38-
const Size intrinsic = GetNativeIntrinsicSize();
95+
const Size intrinsic = IntrinsicSize();
3996
return Rectangle{0.0, 0.0, intrinsic.width, intrinsic.height};
4097
}
4198

@@ -50,14 +107,7 @@ void View::Impl::Relayout() {
50107
std::vector<LayoutChild> children;
51108
children.reserve(subviews.size());
52109
for (const auto& subview : subviews) {
53-
auto& child = *subview->pimpl_;
54-
LayoutChild entry;
55-
entry.visible = child.visible;
56-
entry.preferred = child.preferred_size;
57-
entry.intrinsic = child.visible ? child.GetNativeIntrinsicSize() : Size{0.0, 0.0};
58-
entry.flex = child.flex;
59-
entry.alignment = child.alignment;
60-
children.push_back(entry);
110+
children.push_back(subview->pimpl_->AsLayoutChild());
61111
}
62112
const Rectangle own = GetNativeFrame();
63113
const auto frames = ComputeStackLayout(layout, Size{own.width, own.height}, padding, spacing,
@@ -222,7 +272,7 @@ Size View::GetPreferredSize() const {
222272
}
223273

224274
Size View::GetIntrinsicSize() const {
225-
return pimpl_->GetNativeIntrinsicSize();
275+
return pimpl_->IntrinsicSize();
226276
}
227277

228278
void View::SetFlex(double flex) {

‎src/view.h‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -437,9 +437,15 @@ class View : public EventEmitter<ViewEvent>,
437437
Size GetPreferredSize() const;
438438

439439
/**
440-
* @brief Gets the size the native control wants to be.
440+
* @brief Gets the size the view wants to be when nothing stretches it.
441441
*
442-
* @return The intrinsic size in logical points; zero for a container.
442+
* A Row or Column parent uses it on every axis where SetPreferredSize()
443+
* left 0, so nested rows and columns size to their content.
444+
*
445+
* @return The intrinsic size in logical points: the native control's own
446+
* size; for a Row or Column container, what its visible subviews
447+
* need at their preferred or intrinsic sizes, plus spacing and
448+
* padding; zero for an Absolute container.
443449
*/
444450
Size GetIntrinsicSize() const;
445451

‎src/view_impl.h‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
#pragma once
22

3+
#include <cstdint>
34
#include <memory>
45
#include <optional>
56
#include <string>
67
#include <vector>
78

89
#include "view.h"
10+
#include "view_layout.h"
911
#include "window.h"
1012

1113
namespace nativeapi {
@@ -60,6 +62,11 @@ class View::Impl {
6062
static void InitializeRoot(const std::shared_ptr<View>& view, std::weak_ptr<Window> window);
6163
/// Called by a control after a change that alters its intrinsic size.
6264
void InvalidateIntrinsicSize();
65+
/// What GetIntrinsicSize() reports: the control's own size, or for a Row or
66+
/// Column container with subviews the size its content needs.
67+
Size IntrinsicSize() const;
68+
/// What a Row or Column parent needs to know about this view.
69+
LayoutChild AsLayoutChild() const;
6370
/// Called by a platform that replaced the native view: keeps `native` and
6471
/// the tree's z-order bookkeeping consistent.
6572
void NativeReplaced(void* replacement) { native = replacement; }
@@ -95,6 +102,9 @@ class View::Impl {
95102
/// called, otherwise the intrinsic size at the origin.
96103
Rectangle AbsoluteFrame() const;
97104
bool visible = true;
105+
/// IntrinsicSize() of the layout pass numbered `measured_pass` (view.cpp).
106+
mutable Size measured{0.0, 0.0};
107+
mutable uint64_t measured_pass = 0;
98108
Color background_color{0, 0, 0, 0};
99109
std::optional<std::string> tooltip;
100110

@@ -107,7 +117,8 @@ class View::Impl {
107117
/// its parent pointer; the platform detaches or parks the native child).
108118
void SetNativeFrame(Rectangle frame);
109119
Rectangle GetNativeFrame() const;
110-
/// The size the control wants; zero for a container.
120+
/// The size the native control wants; zero for a container, whose size
121+
/// comes from its content instead (IntrinsicSize()).
111122
Size GetNativeIntrinsicSize() const;
112123
/// Reparents `child`'s native view under this one at z-index `index`.
113124
/// Called after `subviews` already holds the child at `index`, so the

‎src/view_layout.cpp‎

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ double NonNegative(double value) {
1010
return value < 0.0 ? 0.0 : value;
1111
}
1212

13+
double Natural(double preferred, double intrinsic) {
14+
return preferred > 0.0 ? preferred : intrinsic;
15+
}
16+
1317
} // namespace
1418

1519
std::vector<Rectangle> ComputeStackLayout(ViewLayout layout, Size container, EdgeInsets padding,
@@ -33,12 +37,10 @@ std::vector<Rectangle> ComputeStackLayout(ViewLayout layout, Size container, Edg
3337
auto main_of = [&](Size size) { return row ? size.width : size.height; };
3438
auto cross_of = [&](Size size) { return row ? size.height : size.width; };
3539
auto natural_main = [&](const LayoutChild& child) {
36-
const double preferred = main_of(child.preferred);
37-
return preferred > 0.0 ? preferred : main_of(child.intrinsic);
40+
return Natural(main_of(child.preferred), main_of(child.intrinsic));
3841
};
3942
auto natural_cross = [&](const LayoutChild& child) {
40-
const double preferred = cross_of(child.preferred);
41-
return preferred > 0.0 ? preferred : cross_of(child.intrinsic);
43+
return Natural(cross_of(child.preferred), cross_of(child.intrinsic));
4244
};
4345

4446
size_t visible_count = 0;
@@ -96,4 +98,32 @@ std::vector<Rectangle> ComputeStackLayout(ViewLayout layout, Size container, Edg
9698
return frames;
9799
}
98100

101+
Size ComputeStackContentSize(ViewLayout layout, EdgeInsets padding, double spacing,
102+
const std::vector<LayoutChild>& children) {
103+
if (layout == ViewLayout::Absolute) {
104+
return Size{0.0, 0.0};
105+
}
106+
const bool row = layout == ViewLayout::Row;
107+
double main_total = 0.0;
108+
double cross_max = 0.0;
109+
size_t visible_count = 0;
110+
for (const auto& child : children) {
111+
if (!child.visible) {
112+
continue;
113+
}
114+
++visible_count;
115+
const double width = Natural(child.preferred.width, child.intrinsic.width);
116+
const double height = Natural(child.preferred.height, child.intrinsic.height);
117+
main_total += row ? width : height;
118+
cross_max = std::max(cross_max, row ? height : width);
119+
}
120+
if (visible_count > 1) {
121+
main_total += spacing * static_cast<double>(visible_count - 1);
122+
}
123+
const double horizontal = padding.left + padding.right;
124+
const double vertical = padding.top + padding.bottom;
125+
return row ? Size{main_total + horizontal, cross_max + vertical}
126+
: Size{cross_max + horizontal, main_total + vertical};
127+
}
128+
99129
} // namespace nativeapi

‎src/view_layout.h‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,13 @@ struct LayoutChild {
3232
std::vector<Rectangle> ComputeStackLayout(ViewLayout layout, Size container, EdgeInsets padding,
3333
double spacing, const std::vector<LayoutChild>& children);
3434

35+
/**
36+
* The size a Row or Column needs to give every visible child its preferred or
37+
* intrinsic size (flex children included) without stretching: the children end
38+
* to end plus spacing on the main axis, the largest child on the cross axis,
39+
* padding around both. Zero for Absolute, whose children place themselves.
40+
*/
41+
Size ComputeStackContentSize(ViewLayout layout, EdgeInsets padding, double spacing,
42+
const std::vector<LayoutChild>& children);
43+
3544
} // namespace nativeapi

‎tests/CMakeLists.txt‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ add_executable(window_shape_test window_shape_test.cpp)
66
target_link_libraries(window_shape_test PRIVATE nativeapi)
77
add_test(NAME window_shape_test COMMAND window_shape_test)
88

9+
add_executable(view_layout_test view_layout_test.cpp)
10+
target_link_libraries(view_layout_test PRIVATE nativeapi)
11+
add_test(NAME view_layout_test COMMAND view_layout_test)
12+
913
add_executable(window_shadow_blur_test window_shadow_blur_test.cpp)
1014
target_link_libraries(window_shadow_blur_test PRIVATE nativeapi)
1115
add_test(NAME window_shadow_blur_test COMMAND window_shadow_blur_test)

0 commit comments

Comments
 (0)