Skip to content

Commit af00e73

Browse files
authored
feat(tabs): iconSource support for sys:// and font:// icons (#10783)
1 parent f8e15fd commit af00e73

File tree

10 files changed

+175
-22
lines changed

10 files changed

+175
-22
lines changed

apps/toolbox/src/main-page.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
<Button text="scroll-view" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
2525
<Button text="status-bar" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
2626
<Button text="switch" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
27+
<Button text="tabview" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
2728
<Button text="touch-animations" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
2829
<Button text="transitions" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
2930
<Button text="vector-image" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />

apps/toolbox/src/pages/tabview.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { EventData, Observable, Page, Color, TabView, TabViewItem } from '@nativescript/core';
2+
3+
class TabViewDemoModel extends Observable {
4+
private page: Page;
5+
private tabView: TabView;
6+
7+
init(page: Page) {
8+
this.page = page;
9+
this.tabView = page.getViewById('demoTabView') as TabView;
10+
11+
// Ensure initial font icon family so initial font:// icons render as expected
12+
this.applyFontFamily('ns-playground-font');
13+
14+
// Give an initial color to demonstrate colorization of font icons
15+
this.applyItemColor(new Color('#65ADF1'));
16+
}
17+
18+
useSysIcons = () => {
19+
if (!this.tabView || !this.tabView.items) return;
20+
21+
// Common SF Symbol names on iOS. Android will not render sys:// and this is expected.
22+
const sysIcons = ['house.fill', 'star.fill', 'gearshape.fill'];
23+
this.setIcons(sysIcons.map((name) => `sys://${name}`));
24+
};
25+
26+
useFontIcons = () => {
27+
if (!this.tabView || !this.tabView.items) return;
28+
29+
// Use simple glyphs A/B/C for reliability across fonts
30+
const fontIcons = ['A', 'B', 'C'].map((c) => `font://${c}`);
31+
this.setIcons(fontIcons);
32+
this.applyFontFamily('ns-playground-font');
33+
};
34+
35+
clearIcons = () => {
36+
if (!this.tabView || !this.tabView.items) return;
37+
this.tabView.items.forEach((item) => {
38+
(item as TabViewItem).iconSource = undefined;
39+
});
40+
};
41+
42+
private setIcons(iconSources: string[]) {
43+
const items = this.tabView.items as TabViewItem[];
44+
for (let i = 0; i < items.length; i++) {
45+
items[i].iconSource = iconSources[i % iconSources.length];
46+
}
47+
}
48+
49+
private applyFontFamily(family: string) {
50+
if (!this.tabView || !this.tabView.items) return;
51+
(this.tabView.items as TabViewItem[]).forEach((item) => {
52+
// Use indexer to avoid TS typing gap in core .d.ts
53+
(item as any)['iconFontFamily'] = family; // explicit per-item
54+
});
55+
}
56+
57+
private applyItemColor(color: Color) {
58+
(this.tabView.items as TabViewItem[]).forEach((item) => {
59+
(item as TabViewItem).style.color = color;
60+
});
61+
}
62+
}
63+
64+
const vm = new TabViewDemoModel();
65+
66+
export function navigatingTo(args: EventData) {
67+
const page = args.object as Page;
68+
page.bindingContext = vm;
69+
vm.init(page);
70+
}

apps/toolbox/src/pages/tabview.xml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<Page xmlns="http://schemas.nativescript.org/tns.xsd" navigatingTo="navigatingTo" class="page">
2+
<Page.actionBar>
3+
<ActionBar title="TabView" class="action-bar" iosLargeTitle="true" iosShadow="false" />
4+
</Page.actionBar>
5+
6+
7+
<TabView id="demoTabView" selectedIndex="0" androidTabsPosition="top" class="m-t-10">
8+
<!-- First tab -->
9+
<TabViewItem title="First" iconSource="font://&#x41;">
10+
<StackLayout padding="20">
11+
<Label class="description" textWrap="true" text="Test TabViewItem iconSource with sys:// (iOS SF Symbols) and font:// (custom/font glyph)." />
12+
13+
<GridLayout columns="*, *" rows="auto, auto" class="m-y-10">
14+
<Button text="Use sys:// icons" col="0" row="0" tap="{{ useSysIcons }}" class="btn btn-primary" />
15+
<Button text="Use font:// icons" col="1" row="0" tap="{{ useFontIcons }}" class="btn btn-primary" />
16+
<Button text="Clear icons" colSpan="2" row="1" tap="{{ clearIcons }}" class="btn btn-primary" />
17+
</GridLayout>
18+
19+
<Label textWrap="true" class="t-12" text="Note: sys:// icons are iOS-only and map to SF Symbols via UIImage.systemImageNamed. On Android, sys:// will not render and is expected to show no icon." />
20+
</StackLayout>
21+
</TabViewItem>
22+
23+
<!-- Second tab -->
24+
<TabViewItem title="Second" iconSource="font://&#x42;">
25+
<Label text="Second Tab Content" textAlignment="center" verticalAlignment="center" />
26+
</TabViewItem>
27+
28+
<!-- Third tab -->
29+
<TabViewItem title="Third" iconSource="font://&#x43;">
30+
<Label text="Third Tab Content" textAlignment="center" verticalAlignment="center" />
31+
</TabViewItem>
32+
</TabView>
33+
</Page>

packages/core/image-source/index.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,13 @@ export class ImageSource {
6565
* Loads this instance from the specified system image name.
6666
* @param name the name of the system image
6767
*/
68-
static fromSystemImageSync(name: string, instance: ImageBase): ImageSource;
68+
static fromSystemImageSync(name: string, instance?: ImageBase): ImageSource;
6969

7070
/**
7171
* Loads this instance from the specified system image name asynchronously.
7272
* @param name the name of the system image
7373
*/
74-
static fromSystemImage(name: string, instance: ImageBase): Promise<ImageSource>;
74+
static fromSystemImage(name: string, instance?: ImageBase): Promise<ImageSource>;
7575

7676
/**
7777
* Loads this instance from the specified file.

packages/core/ui/styling/style/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ export class Style extends Observable {
106106
}
107107

108108
public fontInternal: Font;
109+
public iconFontFamily: string;
109110
/**
110111
* This property ensures inheritance of a11y scale among views.
111112
*/

packages/core/ui/tab-view/index.android.ts

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { Trace } from '../../trace';
99
import { Color } from '../../color';
1010
import { fontSizeProperty, fontInternalProperty } from '../styling/style-properties';
1111
import { RESOURCE_PREFIX, android as androidUtils, layout } from '../../utils';
12+
import { FONT_PREFIX, isFontIconURI } from '../../utils/common';
1213
import { Frame } from '../frame';
1314
import { getNativeApp } from '../../application/helpers-common';
1415
import { AndroidHelper } from '../core/view';
@@ -292,19 +293,31 @@ function createTabItemSpec(item: TabViewItem): org.nativescript.widgets.TabItemS
292293
result.title = item.title;
293294

294295
if (item.iconSource) {
295-
if (item.iconSource.indexOf(RESOURCE_PREFIX) === 0) {
296-
result.iconId = androidUtils.resources.getDrawableId(item.iconSource.substr(RESOURCE_PREFIX.length));
297-
if (result.iconId === 0) {
298-
traceMissingIcon(item.iconSource);
299-
}
300-
} else {
301-
const is = ImageSource.fromFileOrResourceSync(item.iconSource);
296+
const addDrawable = (is: ImageSource) => {
302297
if (is) {
303298
// TODO: Make this native call that accepts string so that we don't load Bitmap in JS.
304299
result.iconDrawable = new android.graphics.drawable.BitmapDrawable(appResources, is.android);
305300
} else {
306301
traceMissingIcon(item.iconSource);
307302
}
303+
};
304+
if (item.iconSource.indexOf(RESOURCE_PREFIX) === 0) {
305+
result.iconId = androidUtils.resources.getDrawableId(item.iconSource.slice(RESOURCE_PREFIX.length));
306+
if (result.iconId === 0) {
307+
traceMissingIcon(item.iconSource);
308+
}
309+
} else if (isFontIconURI(item.iconSource)) {
310+
// Allow specifying a separate font family for the icon via style.iconFontFamily.
311+
let iconFont: any = item.style.fontInternal;
312+
const iconFontFamily = item.iconFontFamily || item.style.iconFontFamily;
313+
if (iconFontFamily) {
314+
const baseFont = item.style.fontInternal || Font.default;
315+
iconFont = baseFont.withFontFamily(iconFontFamily);
316+
}
317+
const is = ImageSource.fromFontIconCodeSync(item.iconSource.slice(FONT_PREFIX.length), iconFont, item.style.color);
318+
addDrawable(is);
319+
} else {
320+
addDrawable(ImageSource.fromFileOrResourceSync(item.iconSource));
308321
}
309322
}
310323

packages/core/ui/tab-view/index.ios.ts

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ import { CoreTypes } from '../../core-types';
1212
import { ImageSource } from '../../image-source';
1313
import { profile } from '../../profiling';
1414
import { Frame } from '../frame';
15-
import { layout } from '../../utils';
15+
import { layout } from '../../utils/layout-helper';
16+
import { FONT_PREFIX, isFontIconURI, isSystemURI, SYSTEM_PREFIX } from '../../utils/common';
1617
import { SDK_VERSION } from '../../utils/constants';
1718
import { Device } from '../../platform';
1819
export * from './tab-view-common';
@@ -239,7 +240,7 @@ export class TabViewItem extends TabViewItemBase {
239240
const parent = <TabView>this.parent;
240241
const controller = this.__controller;
241242
if (parent && controller) {
242-
const icon = parent._getIcon(this.iconSource);
243+
const icon = parent._getIcon(this);
243244
const index = parent.items.indexOf(this);
244245
const title = getTransformedText(this.title, this.style.textTransform);
245246

@@ -456,7 +457,7 @@ export class TabView extends TabViewBase {
456457

457458
items.forEach((item, i) => {
458459
const controller = this.getViewController(item);
459-
const icon = this._getIcon(item.iconSource);
460+
const icon = this._getIcon(item);
460461
const tabBarItem = UITabBarItem.alloc().initWithTitleImageTag(item.title || '', icon, i);
461462
updateTitleAndIconPositions(item, tabBarItem, controller);
462463

@@ -492,20 +493,36 @@ export class TabView extends TabViewBase {
492493
}
493494
}
494495

495-
public _getIcon(iconSource: string): UIImage {
496-
if (!iconSource) {
496+
public _getIcon(item: TabViewItem): UIImage {
497+
if (!item || !item.iconSource) {
497498
return null;
498499
}
499500

500-
let image: UIImage = this._iconsCache[iconSource];
501+
let image: UIImage = this._iconsCache[item.iconSource];
501502
if (!image) {
502-
const is = ImageSource.fromFileOrResourceSync(iconSource);
503+
let is: ImageSource;
504+
if (isSystemURI(item.iconSource)) {
505+
is = ImageSource.fromSystemImageSync(item.iconSource.slice(SYSTEM_PREFIX.length));
506+
} else if (isFontIconURI(item.iconSource)) {
507+
// Allow specifying a separate font family for the icon via style.iconFontFamily.
508+
// If provided, construct a Font from the family and (optionally) size from fontInternal.
509+
let iconFont = item.style.fontInternal;
510+
const iconFontFamily = item.iconFontFamily || item.style.iconFontFamily;
511+
if (iconFontFamily) {
512+
// Preserve size/style from existing fontInternal if present.
513+
const baseFont = item.style.fontInternal || Font.default;
514+
iconFont = baseFont.withFontFamily(iconFontFamily);
515+
}
516+
is = ImageSource.fromFontIconCodeSync(item.iconSource.slice(FONT_PREFIX.length), iconFont, item.style.color);
517+
} else {
518+
is = ImageSource.fromFileOrResourceSync(item.iconSource);
519+
}
503520
if (is && is.ios) {
504521
const originalRenderedImage = is.ios.imageWithRenderingMode(this._getIconRenderingMode());
505-
this._iconsCache[iconSource] = originalRenderedImage;
522+
this._iconsCache[item.iconSource] = originalRenderedImage;
506523
image = originalRenderedImage;
507524
} else {
508-
traceMissingIcon(iconSource);
525+
traceMissingIcon(item.iconSource);
509526
}
510527
}
511528

packages/core/ui/tab-view/tab-view-common.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export abstract class TabViewItemBase extends ViewBase implements TabViewItemDef
1515
private _title = '';
1616
private _view: View;
1717
private _iconSource: string;
18+
iconFontFamily: string;
1819

1920
get textTransform(): CoreTypes.TextTransformType {
2021
return this.style.textTransform;
@@ -287,6 +288,12 @@ export const tabTextColorProperty = new CssProperty<Style, Color>({
287288
});
288289
tabTextColorProperty.register(Style);
289290

291+
export const iconFontFamilyProperty = new CssProperty<Style, string>({
292+
name: 'iconFontFamily',
293+
cssName: 'icon-font-family',
294+
});
295+
iconFontFamilyProperty.register(Style);
296+
290297
export const tabBackgroundColorProperty = new CssProperty<Style, Color>({
291298
name: 'tabBackgroundColor',
292299
cssName: 'tab-background-color',

packages/core/utils/common.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ export * from './mainthread-helper';
66
export * from './macrotask-scheduler';
77
export * from './utils-shared';
88

9+
export const FILE_PREFIX = 'file:///';
10+
export const FONT_PREFIX = 'font://';
911
export const RESOURCE_PREFIX = 'res://';
1012
export const SYSTEM_PREFIX = 'sys://';
11-
export const FILE_PREFIX = 'file:///';
1213

1314
export function escapeRegexSymbols(source: string): string {
1415
const escapeRegex = /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g;
@@ -84,10 +85,14 @@ export function isFontIconURI(uri: string): boolean {
8485
if (!types.isString(uri)) {
8586
return false;
8687
}
88+
return uri.trim().startsWith(FONT_PREFIX);
89+
}
8790

88-
const firstSegment = uri.trim().split('//')[0];
89-
90-
return firstSegment && firstSegment.indexOf('font:') === 0;
91+
export function isSystemURI(uri: string): boolean {
92+
if (!types.isString(uri)) {
93+
return false;
94+
}
95+
return uri.trim().startsWith(SYSTEM_PREFIX);
9196
}
9297

9398
export function isDataURI(uri: string): boolean {

packages/core/utils/index.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,12 @@ export function mainThreadify(func: Function): (...args: any[]) => void;
7575
*/
7676
export function isFontIconURI(uri: string): boolean;
7777

78+
/**
79+
* Returns true if the specified URI is a system URI like "sys://...".
80+
* @param uri The URI.
81+
*/
82+
export function isSystemURI(uri: string): boolean;
83+
7884
/**
7985
* Returns true if the specified path points to a resource or local file.
8086
* @param path The path.

0 commit comments

Comments
 (0)