Skip to content

Commit bc97fad

Browse files
committed
feat(linux): ListView on GtkListView
Virtualized rows: a GtkStringList of placeholders backs a GtkListView, and each bound row hosts a NativeScript view from the item template (itemLoading, template selector, rowHeight, itemTap, loadMoreItems, scrollToIndex). Views a parent hosts in a GTK widget (list rows, ActionBar.titleView) now answer GTK's measure with a NativeScript measure and lay themselves out.
1 parent 8a88bbd commit bc97fad

4 files changed

Lines changed: 255 additions & 12 deletions

File tree

‎packages/core/ui/action-bar/index.linux.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ export class ActionBar extends ActionBarBase {
7676

7777
public _addViewToNativeVisualTree(child: View): boolean {
7878
if (child === this.titleView && child.nativeViewProtected && this.nativeViewProtected) {
79+
(child as any)._linuxHosted = true;
7980
gtk_center_box_set_center_widget(this.nativeViewProtected, child.nativeViewProtected);
8081
return true;
8182
}

‎packages/core/ui/core/view/gtk-layout.linux.ts‎

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ export interface LinuxLayoutView {
1515
_linuxTransform(width: number, height: number): any;
1616
/** Runs the NativeScript layout pass for a root view. */
1717
_linuxLayoutRoot(width: number, height: number): void;
18+
/** NativeScript size along `orientation` (for a GTK parent that sizes its child). */
19+
_linuxMeasureHosted(orientation: number, forSize: number): number;
20+
/** Set by parents that host the view in a GTK widget without laying it out. */
21+
_linuxHosted?: boolean;
1822
parent?: unknown;
1923
}
2024

@@ -34,21 +38,28 @@ export function viewForWidget(widget: any): LinuxLayoutView | undefined {
3438

3539
/**
3640
* Roots run the layout pass themselves: views without a NativeScript parent, and
37-
* views a parent hosts in a GTK widget without laying them out (e.g. ActionBar.titleView).
41+
* hosted views (placed in a GTK widget by a parent that doesn't lay them out:
42+
* ActionBar.titleView, ListView rows).
3843
*/
3944
function isRoot(view: LinuxLayoutView): boolean {
40-
return !view.parent || !view._linuxFrame;
45+
return !view.parent || !!view._linuxHosted;
4146
}
4247

43-
/** Laid out by NativeScript but placed in a GTK container (e.g. a ScrollView's viewport). */
44-
function frameSizeFor(widget: any, orientation: number): number {
48+
/** Size a GTK parent should give the container. */
49+
function sizeFor(widget: any, orientation: number, forSize: number): number {
4550
const view = viewForWidget(widget);
46-
const frame = view?._linuxFrame;
4751
const parent = gtk_widget_get_parent(widget);
48-
if (!frame || !parent || viewForWidget(parent)) {
52+
if (!view || !parent || viewForWidget(parent)) {
53+
// our containers allocate their children explicitly
4954
return 0;
5055
}
51-
return orientation === GtkOrientation.HORIZONTAL ? frame[2] : frame[3];
56+
if (view._linuxHosted) {
57+
return view._linuxMeasureHosted(orientation, forSize);
58+
}
59+
// laid out by NativeScript but placed in a GTK container (a ScrollView's viewport);
60+
// window content takes the window's size
61+
const frame = view.parent ? view._linuxFrame : null;
62+
return frame ? (orientation === GtkOrientation.HORIZONTAL ? frame[2] : frame[3]) : 0;
5263
}
5364

5465
const minimum = new interop.Reference(interop.types.int32, 0);
@@ -76,8 +87,8 @@ function layoutCallbacks() {
7687
requestMode: () => GtkSizeRequestMode.CONSTANT_SIZE,
7788
// containers take whatever size they are given (the window can shrink freely),
7889
// except inside GTK containers that size them (scrolling viewports)
79-
measure: (widget: any, orientation: number, _forSize: number, min: any, nat: any) => {
80-
const size = frameSizeFor(widget, orientation);
90+
measure: (widget: any, orientation: number, forSize: number, min: any, nat: any) => {
91+
const size = sizeFor(widget, orientation, forSize);
8192
if (min) new interop.Reference(interop.types.int32, min).value = size;
8293
if (nat) new interop.Reference(interop.types.int32, nat).value = size;
8394
},

‎packages/core/ui/core/view/index.linux.ts‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,21 @@ export class View extends ViewCommon {
169169
}
170170
}
171171

172+
/** @internal NativeScript size of a hosted view, for the GTK widget hosting it. */
173+
_linuxMeasureHosted(orientation: number, forSize: number): number {
174+
const unspecified = layout.makeMeasureSpec(0, layout.UNSPECIFIED);
175+
inLayoutPass++;
176+
try {
177+
if (orientation === GtkOrientation.HORIZONTAL) {
178+
return View.measureChild(null, this, unspecified, unspecified).measuredWidth;
179+
}
180+
const width = forSize >= 0 ? layout.makeMeasureSpec(forSize, layout.EXACTLY) : unspecified;
181+
return View.measureChild(null, this, width, unspecified).measuredHeight;
182+
} finally {
183+
inLayoutPass--;
184+
}
185+
}
186+
172187
/** @internal Transform applied when the parent allocates this view. */
173188
_linuxTransform(width: number, height: number): any {
174189
const rotate = this.rotate || 0;
Lines changed: 219 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,226 @@
11
export * from './list-view-common';
22

3-
import { ListViewBase } from './list-view-common';
3+
import type { ItemEventData } from '.';
4+
import { ListViewBase, separatorColorProperty, itemTemplatesProperty } from './list-view-common';
5+
import { View, type KeyedTemplate } from '../core/view';
6+
import { StackLayout } from '../layouts/stack-layout';
7+
import { ProxyViewContainer } from '../proxy-view-container';
8+
import { Color } from '../../color';
9+
import { connect, addressOf } from '../../utils/gtk.linux';
10+
import { setWidgetScopedCss } from '../styling/gtk-style.linux';
411

5-
// TODO(linux): placeholder until the GTK implementation lands
12+
interface Row {
13+
view: View;
14+
template: string;
15+
}
16+
17+
/**
18+
* GtkListView (virtualized, rows are recycled) in a GtkScrolledWindow. The
19+
* model is a GtkStringList of placeholders; each bound row hosts a NativeScript
20+
* view created from the item template and bound to the data item.
21+
*/
622
export class ListView extends ListViewBase {
23+
private _listView: any;
24+
private _model: any;
25+
private _rows = new Map<number, Row>();
26+
private _disconnect: (() => void)[] = [];
27+
private _count = 0;
28+
private _loadMoreAt = -1;
29+
730
createNativeView() {
8-
return gtk_label_new('ListView');
31+
const scroll: any = gtk_scrolled_window_new();
32+
this._model = gtk_string_list_new(null);
33+
const factory: any = gtk_signal_list_item_factory_new();
34+
this._listView = gtk_list_view_new(gtk_no_selection_new(this._model), factory);
35+
gtk_list_view_set_single_click_activate(this._listView, 1);
36+
gtk_scrolled_window_set_policy(scroll, GtkPolicyType.NEVER, GtkPolicyType.AUTOMATIC);
37+
gtk_scrolled_window_set_child(scroll, this._listView);
38+
39+
const owner = new WeakRef(this);
40+
this._disconnect.push(
41+
connect(factory, 'bind', (_f: any, item: any) => owner.deref()?._bind(item), { args: 1 }),
42+
connect(factory, 'teardown', (_f: any, item: any) => owner.deref()?._teardown(item), { args: 1 }),
43+
connect(this._listView, 'activate', (_lv: any, position: number) => owner.deref()?._onActivate(position), { types: ['uint'] }),
44+
connect(gtk_scrolled_window_get_vadjustment(scroll), 'value-changed', (adjustment: any) => owner.deref()?._onScroll(adjustment)),
45+
);
46+
return scroll;
47+
}
48+
49+
get linux(): any {
50+
return this._listView;
51+
}
52+
53+
initNativeView(): void {
54+
super.initNativeView();
55+
this.refresh();
56+
}
57+
58+
disposeNativeView(): void {
59+
for (const disconnect of this._disconnect.splice(0)) {
60+
disconnect();
61+
}
62+
this._clearRows();
63+
this._listView = this._model = null;
64+
super.disposeNativeView();
65+
}
66+
67+
get _childrenCount(): number {
68+
return this._rows.size;
69+
}
70+
71+
public eachChildView(callback: (child: View) => boolean): void {
72+
for (const row of this._rows.values()) {
73+
if (!callback(row.view)) {
74+
break;
75+
}
76+
}
77+
}
78+
79+
// rows live in GtkListItems, not in our native view
80+
public _addViewToNativeVisualTree(child: View): boolean {
81+
return true;
82+
}
83+
84+
public _removeViewFromNativeVisualTree(child: View): void {
85+
child._isAddedToNativeVisualTree = false;
86+
}
87+
88+
private _itemCount(): number {
89+
const items: any = this.items;
90+
return items ? items.length : 0;
91+
}
92+
93+
public refresh() {
94+
if (!this._model) {
95+
return;
96+
}
97+
const count = this._itemCount();
98+
if (count > this._count) {
99+
for (let i = this._count; i < count; i++) {
100+
gtk_string_list_append(this._model, '');
101+
}
102+
} else if (count < this._count) {
103+
gtk_string_list_splice(this._model, count, this._count - count, null);
104+
}
105+
this._count = count;
106+
this._loadMoreAt = -1;
107+
// rebind the visible rows to the current data
108+
if (count) {
109+
g_list_model_items_changed(this._model, 0, count, count);
110+
}
111+
}
112+
113+
public scrollToIndex(index: number) {
114+
if (this._listView && index >= 0 && index < this._count) {
115+
gtk_widget_activate_action(this._listView, 'list.scroll-to-item', 'u', index);
116+
}
117+
}
118+
119+
public scrollToIndexAnimated(index: number) {
120+
this.scrollToIndex(index);
121+
}
122+
123+
public isItemAtIndexVisible(index: number): boolean {
124+
for (const row of this._rows.values()) {
125+
if ((row.view as any)._listViewItemIndex === index) {
126+
return !!row.view.nativeViewProtected && !!gtk_widget_get_mapped(row.view.nativeViewProtected);
127+
}
128+
}
129+
return false;
130+
}
131+
132+
_bind(item: any): void {
133+
const index = gtk_list_item_get_position(item);
134+
const key = addressOf(item);
135+
const template: KeyedTemplate = this._getItemTemplate(index);
136+
let row = this._rows.get(key);
137+
if (row && row.template !== template.key) {
138+
this._removeRow(key);
139+
row = null;
140+
}
141+
let view = row?.view ?? template.createView();
142+
const args = <ItemEventData>{ eventName: ListViewBase.itemLoadingEvent, object: this, index, view, ios: undefined, android: undefined };
143+
this.notify(args);
144+
view = args.view || this._getDefaultItemContent(index);
145+
if (view instanceof ProxyViewContainer) {
146+
const stack = new StackLayout();
147+
stack.addChild(view);
148+
view = stack;
149+
}
150+
if (row && row.view !== view) {
151+
this._removeRow(key);
152+
row = null;
153+
}
154+
this._prepareItem(view, index);
155+
(view as any)._listViewItemIndex = index;
156+
if (!row) {
157+
(view as any)._linuxHosted = true;
158+
if (this._effectiveRowHeight > 0) {
159+
view.height = this._effectiveRowHeight;
160+
}
161+
this._rows.set(key, { view, template: template.key });
162+
this._addView(view);
163+
gtk_list_item_set_child(item, view.nativeViewProtected);
164+
}
165+
view.requestLayout();
166+
}
167+
168+
_teardown(item: any): void {
169+
this._removeRow(addressOf(item));
170+
}
171+
172+
private _removeRow(key: number): void {
173+
const row = this._rows.get(key);
174+
if (row) {
175+
this._rows.delete(key);
176+
if (row.view.parent === this) {
177+
this._removeView(row.view);
178+
}
179+
}
180+
}
181+
182+
private _clearRows(): void {
183+
for (const key of [...this._rows.keys()]) {
184+
this._removeRow(key);
185+
}
186+
}
187+
188+
_onActivate(position: number): void {
189+
let view: View;
190+
for (const row of this._rows.values()) {
191+
if ((row.view as any)._listViewItemIndex === position) {
192+
view = row.view;
193+
}
194+
}
195+
this.notify(<ItemEventData>{ eventName: ListViewBase.itemTapEvent, object: this, index: position, view, ios: undefined, android: undefined });
196+
}
197+
198+
_onScroll(adjustment: any): void {
199+
const value = gtk_adjustment_get_value(adjustment);
200+
const page = gtk_adjustment_get_page_size(adjustment);
201+
const upper = gtk_adjustment_get_upper(adjustment);
202+
// once per item count, when the end comes into view
203+
if (upper > page && value + page >= upper - 1 && this._loadMoreAt !== this._count) {
204+
this._loadMoreAt = this._count;
205+
this.notify({ eventName: ListViewBase.loadMoreItemsEvent, object: this });
206+
}
207+
}
208+
209+
public _onRowHeightPropertyChanged(oldValue: any, newValue: any) {
210+
for (const row of this._rows.values()) {
211+
row.view.height = this._effectiveRowHeight > 0 ? this._effectiveRowHeight : 'auto';
212+
}
213+
this.refresh();
214+
}
215+
216+
[separatorColorProperty.setNative](value: Color) {
217+
gtk_list_view_set_show_separators(this._listView, value ? 1 : 0);
218+
setWidgetScopedCss(this._listView, 'separator', value instanceof Color ? `& > row{border-bottom:1px solid ${value.linux};}` : '');
219+
}
220+
221+
[itemTemplatesProperty.setNative](value: KeyedTemplate[]) {
222+
this._itemTemplatesInternal = [this._defaultTemplate, ...(value ?? [])];
223+
this._clearRows();
224+
this.refresh();
9225
}
10226
}

0 commit comments

Comments
 (0)