Skip to content
1 change: 1 addition & 0 deletions apps/toolbox/src/main-page.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
<Button text="scroll-view" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
<Button text="status-bar" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
<Button text="switch" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
<Button text="tabview" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
<Button text="touch-animations" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
<Button text="transitions" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
<Button text="vector-image" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
Expand Down
70 changes: 70 additions & 0 deletions apps/toolbox/src/pages/tabview.ts
Original file line number Diff line number Diff line change
@@ -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);
}
33 changes: 33 additions & 0 deletions apps/toolbox/src/pages/tabview.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<Page xmlns="http://schemas.nativescript.org/tns.xsd" navigatingTo="navigatingTo" class="page">
<Page.actionBar>
<ActionBar title="TabView" class="action-bar" iosLargeTitle="true" iosShadow="false" />
</Page.actionBar>


<TabView id="demoTabView" selectedIndex="0" androidTabsPosition="top" class="m-t-10">
<!-- First tab -->
<TabViewItem title="First" iconSource="font://&#x41;">
<StackLayout padding="20">
<Label class="description" textWrap="true" text="Test TabViewItem iconSource with sys:// (iOS SF Symbols) and font:// (custom/font glyph)." />

<GridLayout columns="*, *" rows="auto, auto" class="m-y-10">
<Button text="Use sys:// icons" col="0" row="0" tap="{{ useSysIcons }}" class="btn btn-primary" />
<Button text="Use font:// icons" col="1" row="0" tap="{{ useFontIcons }}" class="btn btn-primary" />
<Button text="Clear icons" colSpan="2" row="1" tap="{{ clearIcons }}" class="btn btn-primary" />
</GridLayout>

<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." />
</StackLayout>
</TabViewItem>

<!-- Second tab -->
<TabViewItem title="Second" iconSource="font://&#x42;">
<Label text="Second Tab Content" textAlignment="center" verticalAlignment="center" />
</TabViewItem>

<!-- Third tab -->
<TabViewItem title="Third" iconSource="font://&#x43;">
<Label text="Third Tab Content" textAlignment="center" verticalAlignment="center" />
</TabViewItem>
</TabView>
</Page>
4 changes: 2 additions & 2 deletions packages/core/image-source/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ImageSource>;
static fromSystemImage(name: string, instance?: ImageBase): Promise<ImageSource>;

/**
* Loads this instance from the specified file.
Expand Down
1 change: 1 addition & 0 deletions packages/core/ui/styling/style/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ export class Style extends Observable {
}

public fontInternal: Font;
public iconFontFamily: string;
/**
* This property ensures inheritance of a11y scale among views.
*/
Expand Down
27 changes: 20 additions & 7 deletions packages/core/ui/tab-view/index.android.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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));
}
}

Expand Down
35 changes: 26 additions & 9 deletions packages/core/ui/tab-view/index.ios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -239,7 +240,7 @@ export class TabViewItem extends TabViewItemBase {
const parent = <TabView>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);

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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);
}
}

Expand Down
7 changes: 7 additions & 0 deletions packages/core/ui/tab-view/tab-view-common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -287,6 +288,12 @@ export const tabTextColorProperty = new CssProperty<Style, Color>({
});
tabTextColorProperty.register(Style);

export const iconFontFamilyProperty = new CssProperty<Style, string>({
name: 'iconFontFamily',
cssName: 'icon-font-family',
});
iconFontFamilyProperty.register(Style);

export const tabBackgroundColorProperty = new CssProperty<Style, Color>({
name: 'tabBackgroundColor',
cssName: 'tab-background-color',
Expand Down
13 changes: 9 additions & 4 deletions packages/core/utils/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ export * from './mainthread-helper';
export * from './macrotask-scheduler';
export * from './utils-shared';

export const FILE_PREFIX = 'file:///';
export const FONT_PREFIX = 'font://';
export const RESOURCE_PREFIX = 'res://';
export const SYSTEM_PREFIX = 'sys://';
export const FILE_PREFIX = 'file:///';

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

const firstSegment = uri.trim().split('//')[0];

return firstSegment && firstSegment.indexOf('font:') === 0;
export function isSystemURI(uri: string): boolean {
if (!types.isString(uri)) {
return false;
}
return uri.trim().startsWith(SYSTEM_PREFIX);
}

export function isDataURI(uri: string): boolean {
Expand Down
6 changes: 6 additions & 0 deletions packages/core/utils/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ export function mainThreadify(func: Function): (...args: any[]) => void;
*/
export function isFontIconURI(uri: string): boolean;

/**
* Returns true if the specified URI is a system URI like "sys://...".
* @param uri The URI.
*/
export function isSystemURI(uri: string): boolean;

/**
* Returns true if the specified path points to a resource or local file.
* @param path The path.
Expand Down