diff --git a/apps/toolbox/src/main-page.xml b/apps/toolbox/src/main-page.xml
index 8440b4ea76..3c72d0b392 100644
--- a/apps/toolbox/src/main-page.xml
+++ b/apps/toolbox/src/main-page.xml
@@ -24,6 +24,7 @@
+
diff --git a/apps/toolbox/src/pages/tabview.ts b/apps/toolbox/src/pages/tabview.ts
new file mode 100644
index 0000000000..8ee399e15d
--- /dev/null
+++ b/apps/toolbox/src/pages/tabview.ts
@@ -0,0 +1,70 @@
+import { EventData, Observable, Page, Color, TabView, TabViewItem } from '@nativescript/core';
+
+class TabViewDemoModel extends Observable {
+ private page: Page;
+ private tabView: TabView;
+
+ init(page: Page) {
+ this.page = page;
+ this.tabView = page.getViewById('demoTabView') as TabView;
+
+ // Ensure initial font icon family so initial font:// icons render as expected
+ this.applyFontFamily('ns-playground-font');
+
+ // Give an initial color to demonstrate colorization of font icons
+ this.applyItemColor(new Color('#65ADF1'));
+ }
+
+ useSysIcons = () => {
+ if (!this.tabView || !this.tabView.items) return;
+
+ // Common SF Symbol names on iOS. Android will not render sys:// and this is expected.
+ const sysIcons = ['house.fill', 'star.fill', 'gearshape.fill'];
+ this.setIcons(sysIcons.map((name) => `sys://${name}`));
+ };
+
+ useFontIcons = () => {
+ if (!this.tabView || !this.tabView.items) return;
+
+ // Use simple glyphs A/B/C for reliability across fonts
+ const fontIcons = ['A', 'B', 'C'].map((c) => `font://${c}`);
+ this.setIcons(fontIcons);
+ this.applyFontFamily('ns-playground-font');
+ };
+
+ clearIcons = () => {
+ if (!this.tabView || !this.tabView.items) return;
+ this.tabView.items.forEach((item) => {
+ (item as TabViewItem).iconSource = undefined;
+ });
+ };
+
+ private setIcons(iconSources: string[]) {
+ const items = this.tabView.items as TabViewItem[];
+ for (let i = 0; i < items.length; i++) {
+ items[i].iconSource = iconSources[i % iconSources.length];
+ }
+ }
+
+ private applyFontFamily(family: string) {
+ if (!this.tabView || !this.tabView.items) return;
+ (this.tabView.items as TabViewItem[]).forEach((item) => {
+ // Use indexer to avoid TS typing gap in core .d.ts
+ (item as any)['iconFontFamily'] = family; // explicit per-item
+ });
+ }
+
+ private applyItemColor(color: Color) {
+ (this.tabView.items as TabViewItem[]).forEach((item) => {
+ (item as TabViewItem).style.color = color;
+ });
+ }
+}
+
+const vm = new TabViewDemoModel();
+
+export function navigatingTo(args: EventData) {
+ const page = args.object as Page;
+ page.bindingContext = vm;
+ vm.init(page);
+}
diff --git a/apps/toolbox/src/pages/tabview.xml b/apps/toolbox/src/pages/tabview.xml
new file mode 100644
index 0000000000..f993e88b54
--- /dev/null
+++ b/apps/toolbox/src/pages/tabview.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/core/image-source/index.d.ts b/packages/core/image-source/index.d.ts
index 5394b68314..95d086d474 100644
--- a/packages/core/image-source/index.d.ts
+++ b/packages/core/image-source/index.d.ts
@@ -65,13 +65,13 @@ export class ImageSource {
* Loads this instance from the specified system image name.
* @param name the name of the system image
*/
- static fromSystemImageSync(name: string, instance: ImageBase): ImageSource;
+ static fromSystemImageSync(name: string, instance?: ImageBase): ImageSource;
/**
* Loads this instance from the specified system image name asynchronously.
* @param name the name of the system image
*/
- static fromSystemImage(name: string, instance: ImageBase): Promise;
+ static fromSystemImage(name: string, instance?: ImageBase): Promise;
/**
* Loads this instance from the specified file.
diff --git a/packages/core/ui/styling/style/index.ts b/packages/core/ui/styling/style/index.ts
index a9d5c02f03..066cc591a8 100644
--- a/packages/core/ui/styling/style/index.ts
+++ b/packages/core/ui/styling/style/index.ts
@@ -106,6 +106,7 @@ export class Style extends Observable {
}
public fontInternal: Font;
+ public iconFontFamily: string;
/**
* This property ensures inheritance of a11y scale among views.
*/
diff --git a/packages/core/ui/tab-view/index.android.ts b/packages/core/ui/tab-view/index.android.ts
index ef7830da18..be11df1e58 100644
--- a/packages/core/ui/tab-view/index.android.ts
+++ b/packages/core/ui/tab-view/index.android.ts
@@ -9,6 +9,7 @@ import { Trace } from '../../trace';
import { Color } from '../../color';
import { fontSizeProperty, fontInternalProperty } from '../styling/style-properties';
import { RESOURCE_PREFIX, android as androidUtils, layout } from '../../utils';
+import { FONT_PREFIX, isFontIconURI } from '../../utils/common';
import { Frame } from '../frame';
import { getNativeApp } from '../../application/helpers-common';
import { AndroidHelper } from '../core/view';
@@ -292,19 +293,31 @@ function createTabItemSpec(item: TabViewItem): org.nativescript.widgets.TabItemS
result.title = item.title;
if (item.iconSource) {
- if (item.iconSource.indexOf(RESOURCE_PREFIX) === 0) {
- result.iconId = androidUtils.resources.getDrawableId(item.iconSource.substr(RESOURCE_PREFIX.length));
- if (result.iconId === 0) {
- traceMissingIcon(item.iconSource);
- }
- } else {
- const is = ImageSource.fromFileOrResourceSync(item.iconSource);
+ const addDrawable = (is: ImageSource) => {
if (is) {
// TODO: Make this native call that accepts string so that we don't load Bitmap in JS.
result.iconDrawable = new android.graphics.drawable.BitmapDrawable(appResources, is.android);
} else {
traceMissingIcon(item.iconSource);
}
+ };
+ if (item.iconSource.indexOf(RESOURCE_PREFIX) === 0) {
+ result.iconId = androidUtils.resources.getDrawableId(item.iconSource.slice(RESOURCE_PREFIX.length));
+ if (result.iconId === 0) {
+ traceMissingIcon(item.iconSource);
+ }
+ } else if (isFontIconURI(item.iconSource)) {
+ // Allow specifying a separate font family for the icon via style.iconFontFamily.
+ let iconFont: any = item.style.fontInternal;
+ const iconFontFamily = item.iconFontFamily || item.style.iconFontFamily;
+ if (iconFontFamily) {
+ const baseFont = item.style.fontInternal || Font.default;
+ iconFont = baseFont.withFontFamily(iconFontFamily);
+ }
+ const is = ImageSource.fromFontIconCodeSync(item.iconSource.slice(FONT_PREFIX.length), iconFont, item.style.color);
+ addDrawable(is);
+ } else {
+ addDrawable(ImageSource.fromFileOrResourceSync(item.iconSource));
}
}
diff --git a/packages/core/ui/tab-view/index.ios.ts b/packages/core/ui/tab-view/index.ios.ts
index 5da4665dd6..7680586c4e 100644
--- a/packages/core/ui/tab-view/index.ios.ts
+++ b/packages/core/ui/tab-view/index.ios.ts
@@ -12,7 +12,8 @@ import { CoreTypes } from '../../core-types';
import { ImageSource } from '../../image-source';
import { profile } from '../../profiling';
import { Frame } from '../frame';
-import { layout } from '../../utils';
+import { layout } from '../../utils/layout-helper';
+import { FONT_PREFIX, isFontIconURI, isSystemURI, SYSTEM_PREFIX } from '../../utils/common';
import { SDK_VERSION } from '../../utils/constants';
import { Device } from '../../platform';
export * from './tab-view-common';
@@ -239,7 +240,7 @@ export class TabViewItem extends TabViewItemBase {
const parent = this.parent;
const controller = this.__controller;
if (parent && controller) {
- const icon = parent._getIcon(this.iconSource);
+ const icon = parent._getIcon(this);
const index = parent.items.indexOf(this);
const title = getTransformedText(this.title, this.style.textTransform);
@@ -456,7 +457,7 @@ export class TabView extends TabViewBase {
items.forEach((item, i) => {
const controller = this.getViewController(item);
- const icon = this._getIcon(item.iconSource);
+ const icon = this._getIcon(item);
const tabBarItem = UITabBarItem.alloc().initWithTitleImageTag(item.title || '', icon, i);
updateTitleAndIconPositions(item, tabBarItem, controller);
@@ -492,20 +493,36 @@ export class TabView extends TabViewBase {
}
}
- public _getIcon(iconSource: string): UIImage {
- if (!iconSource) {
+ public _getIcon(item: TabViewItem): UIImage {
+ if (!item || !item.iconSource) {
return null;
}
- let image: UIImage = this._iconsCache[iconSource];
+ let image: UIImage = this._iconsCache[item.iconSource];
if (!image) {
- const is = ImageSource.fromFileOrResourceSync(iconSource);
+ let is: ImageSource;
+ if (isSystemURI(item.iconSource)) {
+ is = ImageSource.fromSystemImageSync(item.iconSource.slice(SYSTEM_PREFIX.length));
+ } else if (isFontIconURI(item.iconSource)) {
+ // Allow specifying a separate font family for the icon via style.iconFontFamily.
+ // If provided, construct a Font from the family and (optionally) size from fontInternal.
+ let iconFont = item.style.fontInternal;
+ const iconFontFamily = item.iconFontFamily || item.style.iconFontFamily;
+ if (iconFontFamily) {
+ // Preserve size/style from existing fontInternal if present.
+ const baseFont = item.style.fontInternal || Font.default;
+ iconFont = baseFont.withFontFamily(iconFontFamily);
+ }
+ is = ImageSource.fromFontIconCodeSync(item.iconSource.slice(FONT_PREFIX.length), iconFont, item.style.color);
+ } else {
+ is = ImageSource.fromFileOrResourceSync(item.iconSource);
+ }
if (is && is.ios) {
const originalRenderedImage = is.ios.imageWithRenderingMode(this._getIconRenderingMode());
- this._iconsCache[iconSource] = originalRenderedImage;
+ this._iconsCache[item.iconSource] = originalRenderedImage;
image = originalRenderedImage;
} else {
- traceMissingIcon(iconSource);
+ traceMissingIcon(item.iconSource);
}
}
diff --git a/packages/core/ui/tab-view/tab-view-common.ts b/packages/core/ui/tab-view/tab-view-common.ts
index c4a06d0ce3..75730e4a34 100644
--- a/packages/core/ui/tab-view/tab-view-common.ts
+++ b/packages/core/ui/tab-view/tab-view-common.ts
@@ -15,6 +15,7 @@ export abstract class TabViewItemBase extends ViewBase implements TabViewItemDef
private _title = '';
private _view: View;
private _iconSource: string;
+ iconFontFamily: string;
get textTransform(): CoreTypes.TextTransformType {
return this.style.textTransform;
@@ -287,6 +288,12 @@ export const tabTextColorProperty = new CssProperty