diff --git a/appconfig.ts b/appconfig.ts index dbea42f1..2a84bbf9 100644 --- a/appconfig.ts +++ b/appconfig.ts @@ -1,6 +1,6 @@ namespace user_interface_base { export const font = bitmaps.font8 - export let getIcon: (name: string, nullIfMissing: boolean) => Bitmap = null + export let getIcon: (name: string | number , nullIfMissing: boolean) => Bitmap = null export let resolveTooltip: (ariaId: string) => string = null export interface AppInterface { pushScene(scene: Scene): void diff --git a/button.ts b/button.ts index 4f26cbc7..e7d60c2e 100644 --- a/button.ts +++ b/button.ts @@ -1,334 +1,396 @@ namespace user_interface_base { - export class Borders { - constructor( - public top: number, - public bottom: number, - public left: number, - public right: number - ) { } + export class Borders { + constructor( + public top: number, + public bottom: number, + public left: number, + public right: number + ) { } + } + + export class ButtonStyle { + constructor( + public fill: number, + public borders: Borders, + public shadow: boolean + ) { } + } + + + /** + * Parameters for a Button constructor, which controls the: + * background behind the bitmap in the button; whether or not it has a shadow effect; + * and the style of the borders around the button. + */ + export namespace ButtonStyles { + export const ShadowedWhite = new ButtonStyle( + 1, + new Borders(1, 12, 1, 1), + true + ) + export const LightShadowedWhite = new ButtonStyle( + 1, + new Borders(1, 11, 1, 1), + true + ) + export const FlatWhite = new ButtonStyle( + 1, + new Borders(1, 1, 1, 1), + false + ) + /* + export const RectangleWhite = new ButtonStyle( + 1, + new Borders(0, 0, 0, 0), + false + ) + */ + export const BorderedPurple = new ButtonStyle( + 11, + new Borders(12, 12, 12, 12), + false + ) + export const RedBorderedWhite = new ButtonStyle( + 1, + new Borders(2, 2, 2, 2), + false + ) + export const Transparent = new ButtonStyle( + 0, + new Borders(0, 0, 0, 0), + false + ) + } + + export function borderLeft(style: ButtonStyle) { + return style.borders.left ? 1 : 0 + } + + export function borderTop(style: ButtonStyle) { + return style.borders.top ? 1 : 0 + } + + export function borderRight(style: ButtonStyle) { + return style.borders.right ? 1 : 0 + } + + export function borderBottom(style: ButtonStyle) { + return style.borders.bottom ? 1 : 0 + } + + export function borderWidth(style: ButtonStyle) { + return borderLeft(style) + borderRight(style) + } + + export function borderHeight(style: ButtonStyle) { + return borderTop(style) + borderBottom(style) + } + + + /** + * This is currently only extended by Button. + * Some navigators and pickers choose to operate on this instead of buttons for generality. + * See the Button class below. + */ + export class ButtonBase implements IComponent, ISizable, IPlaceable { + public icon: Sprite + private xfrm_: Affine + private style: ButtonStyle + + constructor(x: number, y: number, style: ButtonStyle, parent: Affine) { + this.xfrm_ = new Affine() + this.xfrm.localPos.x = x + this.xfrm.localPos.y = y + this.style = style + this.xfrm.parent = parent } - export class ButtonStyle { - constructor( - public fill: number, - public borders: Borders, - public shadow: boolean - ) { } + public get xfrm() { + return this.xfrm_ } - - export namespace ButtonStyles { - export const ShadowedWhite = new ButtonStyle( - 1, - new Borders(1, 12, 1, 1), - true - ) - export const LightShadowedWhite = new ButtonStyle( - 1, - new Borders(1, 11, 1, 1), - true - ) - export const FlatWhite = new ButtonStyle( - 1, - new Borders(1, 1, 1, 1), - false - ) - /* - export const RectangleWhite = new ButtonStyle( - 1, - new Borders(0, 0, 0, 0), - false - ) - */ - export const BorderedPurple = new ButtonStyle( - 11, - new Borders(12, 12, 12, 12), - false - ) - export const RedBorderedWhite = new ButtonStyle( - 1, - new Borders(2, 2, 2, 2), - false - ) - export const Transparent = new ButtonStyle( - 0, - new Borders(0, 0, 0, 0), - false - ) + public get width() { + return this.bounds.width } - - export function borderLeft(style: ButtonStyle) { - return style.borders.left ? 1 : 0 + public get height() { + return this.bounds.height } - export function borderTop(style: ButtonStyle) { - return style.borders.top ? 1 : 0 + public get bounds() { + // Returns bounds in local space + return Bounds.GrowXY( + this.icon.bounds, + borderLeft(this.style), + borderTop(this.style) + ) } - export function borderRight(style: ButtonStyle) { - return style.borders.right ? 1 : 0 + public get rootXfrm(): Affine { + let xfrm = this.xfrm + while (xfrm.parent) { + xfrm = xfrm.parent + } + return xfrm } - export function borderBottom(style: ButtonStyle) { - return style.borders.bottom ? 1 : 0 + public buildSprite(img: Bitmap) { + this.icon = new Sprite({ + parent: this, + img, + }) + this.icon.xfrm.parent = this.xfrm } - export function borderWidth(style: ButtonStyle) { - return borderLeft(style) + borderRight(style) + public getImage() { + return this.icon.image } - export function borderHeight(style: ButtonStyle) { - return borderTop(style) + borderBottom(style) + public occlusions(bounds: Bounds) { + return this.icon.occlusions(bounds) } - export class ButtonBase implements IComponent, ISizable, IPlaceable { - public icon: Sprite - private xfrm_: Affine - private style: ButtonStyle - - constructor(x: number, y: number, style: ButtonStyle, parent: Affine) { - this.xfrm_ = new Affine() - this.xfrm.localPos.x = x - this.xfrm.localPos.y = y - this.style = style - this.xfrm.parent = parent - } - - public get xfrm() { - return this.xfrm_ - } - public get width() { - return this.bounds.width - } - public get height() { - return this.bounds.height - } - - public get bounds() { - // Returns bounds in local space - return Bounds.GrowXY( - this.icon.bounds, - borderLeft(this.style), - borderTop(this.style) - ) - } - - public get rootXfrm(): Affine { - let xfrm = this.xfrm - while (xfrm.parent) { - xfrm = xfrm.parent - } - return xfrm - } - - public buildSprite(img: Bitmap) { - this.icon = new Sprite({ - parent: this, - img, - }) - this.icon.xfrm.parent = this.xfrm - } - - public getImage() { - return this.icon.image - } - - public occlusions(bounds: Bounds) { - return this.icon.occlusions(bounds) - } - - public setVisible(visible: boolean) { - this.icon.invisible = !visible - if (!visible) { - this.hover(false) - } - } - - public visible() { - return !this.icon.invisible - } + public setVisible(visible: boolean) { + this.icon.invisible = !visible + if (!visible) { + this.hover(false) + } + } - public hover(hov: boolean) { } - public update() { } + public visible() { + return !this.icon.invisible + } - isOffScreenX(): boolean { - return this.icon.isOffScreenX() - } + public hover(hov: boolean) { } + public update() { } - draw() { - this.drawStyle() - this.drawIcon() - } + isOffScreenX(): boolean { + return this.icon.isOffScreenX() + } - private drawIcon() { - this.icon.draw() - } + draw() { + this.drawStyle() + this.drawIcon() + } - private drawStyle() { - if (this.style.fill) - Screen.fillBoundsXfrm( - this.xfrm, - this.icon.bounds, - this.style.fill - ) - if (this.style.borders) - Screen.outlineBoundsXfrm4( - this.xfrm, - this.icon.bounds, - 1, - this.style.borders - ) - if (this.style.shadow) { - Screen.setPixelXfrm( - this.xfrm, - this.icon.bounds.left - 1, - this.icon.bounds.bottom, - this.style.borders.bottom - ) - Screen.setPixelXfrm( - this.xfrm, - this.icon.bounds.right + 1, - this.icon.bounds.bottom, - this.style.borders.bottom - ) - } - } + private drawIcon() { + this.icon.draw() } - export class Button extends ButtonBase { - private iconId: string | Bitmap - private _ariaId: string - public onClick?: (button: Button) => void - public selected: boolean - private dynamicBoundaryColorsOn: boolean - private boundaryColor: number - public state: number[] - public pressable: boolean - - public get ariaId(): string { - return this._ariaId - } + private drawStyle() { + if (this.style.fill) + Screen.fillBoundsXfrm( + this.xfrm, + this.icon.bounds, + this.style.fill + ) + if (this.style.borders) + Screen.outlineBoundsXfrm4( + this.xfrm, + this.icon.bounds, + 1, + this.style.borders + ) + if (this.style.shadow) { + Screen.setPixelXfrm( + this.xfrm, + this.icon.bounds.left - 1, + this.icon.bounds.bottom, + this.style.borders.bottom + ) + Screen.setPixelXfrm( + this.xfrm, + this.icon.bounds.right + 1, + this.icon.bounds.bottom, + this.style.borders.bottom + ) + } + } + } + + + /** + * GUI Button with a bitmap sprite, border colour, bitmap background colour, + * text can be optoinally displayed below the button. + * These buttons are typically used by navigators, + * which map physical button presses over a row or grid of these buttons. + * + * ALL arguments for the Button constructor are optional and have defaults. + */ + export class Button extends ButtonBase { + /** The bitmap to be displayed by the button, if it is a string it will be looked up; see coreAssets.ts */ + private iconId: string | number | Bitmap + /** The text to be displayed below the button, formerly used as a lookup for the text to be displayed. */ + private _ariaId: string + /** + * The action the button should take, typically used to transition between scenes. + * The button parameter is optional. It may be useful if you want to change the boundaryColor, or state, when pressed. + */ + public onClick?: (button: Button) => void + /** + * Used by draw to change boundary colour to .boundaryColor when this is true, and .dynamicBoundaryColorsOn + */ + public selected: boolean + /** + * Do you want the buttons boundary to change to .boundaryColor when pressed? + */ + private dynamicBoundaryColorsOn: boolean + + /** + * used by .dynamicBoundaryColorsOn and .selected, see .draw() + */ + private boundaryColor: number + + /** + * Seldom used, you can store arbitrary data here. + * This might be useful when you choose to pass button with onClick(). + */ + public state: any[] + /** + * Used to disable a button. + */ + public pressable: boolean + + public get ariaId(): string { + return this._ariaId + } - public set ariaId(value: string) { - this._ariaId = value - } + public set ariaId(value: string) { + this._ariaId = value + } - get getLocalX() { return this.xfrm.localPos.x } - get getLocalY() { return this.xfrm.localPos.y } + get getLocalX() { return this.xfrm.localPos.x } + get getLocalY() { return this.xfrm.localPos.y } - set setLocalX(x: number) { this.xfrm.localPos.x = x } - set setLocalY(y: number) { this.xfrm.localPos.y = y } + set setLocalX(x: number) { this.xfrm.localPos.x = x } + set setLocalY(y: number) { this.xfrm.localPos.y = y } - reportAria(force = false) { - const msg: accessibility.TileAccessibilityMessage = { - type: "tile", - value: this.ariaId, - force, - } - accessibility.setLiveContent(msg) - } + reportAria(force = false) { + const msg: accessibility.TileAccessibilityMessage = { + type: "tile", + value: this.ariaId, + force, + } + accessibility.setLiveContent(msg) + } - constructor(opts: { - parent?: IPlaceable - style?: ButtonStyle - icon?: string | Bitmap - ariaId?: string - x?: number - y?: number - onClick?: (button: Button) => void, - dynamicBoundaryColorsOn?: boolean - boundaryColor?: number, - flipIcon?: boolean, - state?: number[] - }) { - super( - (opts.x != null) ? opts.x : 0, - (opts.y != null) ? opts.y : 0, - opts.style || ButtonStyles.Transparent, - opts.parent && opts.parent.xfrm - ) - this.iconId = opts.icon - this._ariaId = (opts.ariaId != null) ? opts.ariaId : "" - this.onClick = opts.onClick - this.buildSprite(this.image_()) - - if (opts.flipIcon) { - this.icon.image = this.icon.image.clone() - this.icon.image.flipY() - } - - this.selected = false - this.pressable = true - - if (opts.dynamicBoundaryColorsOn == null) { - opts.dynamicBoundaryColorsOn = false - } - else { - this.dynamicBoundaryColorsOn = opts.dynamicBoundaryColorsOn - this.boundaryColor = 2 - } - - if (opts.boundaryColor != null) { - this.dynamicBoundaryColorsOn = true - this.boundaryColor = opts.boundaryColor - } - - this.state = opts.state - } + constructor(opts: { + parent?: IPlaceable + style?: ButtonStyle + icon?: string | number | Bitmap + ariaId?: string + x?: number + y?: number + onClick?: (button: Button) => void, + dynamicBoundaryColorsOn?: boolean + boundaryColor?: number, + flipIcon?: boolean, + state?: any[] + }) { + super( + (opts.x != null) ? opts.x : 0, + (opts.y != null) ? opts.y : 0, + opts.style || ButtonStyles.Transparent, + opts.parent && opts.parent.xfrm + ) + this.iconId = opts.icon + this._ariaId = (opts.ariaId != null) ? opts.ariaId : "" + this.onClick = opts.onClick + this.buildSprite(this.image_()) + + if (opts.flipIcon) { + this.icon.image = this.icon.image.clone() + this.icon.image.flipY() + } + + this.selected = false + this.pressable = true + + // Setup dynamic boundary colours. + // This is used by .draw() + // If the button is hovered it is blue, it it is selected (A pressed once) it is green, other button boundaries are red. + // This remains the case even if you are no longer hovering over the button. + // This means that all buttons with dynamicBoundaryColorsOn will have boundaries. + // MicroData/sensorSelect.ts uses this to select multiple buttons. + + if (opts.dynamicBoundaryColorsOn == null) { + opts.dynamicBoundaryColorsOn = false + } + else { + this.dynamicBoundaryColorsOn = opts.dynamicBoundaryColorsOn + this.boundaryColor = 2 // Red + } + + if (opts.boundaryColor != null) { + this.dynamicBoundaryColorsOn = true + this.boundaryColor = opts.boundaryColor + } + + // This is null if not passed in. + this.state = opts.state + } - public getIcon() { - return this.iconId - } + public getIcon() { + return this.iconId + } - public toggleDynamicBoundaryColors() { - this.dynamicBoundaryColorsOn = !this.dynamicBoundaryColorsOn - } + public toggleDynamicBoundaryColors() { + this.dynamicBoundaryColorsOn = !this.dynamicBoundaryColorsOn + } - public toggleSelected(): void { - this.selected = !this.selected - } + public toggleSelected(): void { + this.selected = !this.selected + } - private image_() { - return typeof this.iconId == "string" - ? getIcon(this.iconId, false) - : this.iconId - } + private image_() { + return typeof this.iconId == "string" || typeof this.iconId == "number" + ? getIcon(this.iconId, false) + : this.iconId + } - public setIcon(iconId: string, img?: Bitmap) { - this.iconId = iconId - if (img) this.icon.setImage(img) - else this.buildSprite(this.image_()) - } + public setIcon(iconId: string | number, img?: Bitmap) { + this.iconId = iconId + if (img) this.icon.setImage(img) + else this.buildSprite(this.image_()) + } - public clickable() { - return this.visible() && this.pressable - } + /** + * Is it visible and pressable? + */ + public clickable() { + return this.visible() && this.pressable + } - public click() { - if (!this.clickable()) { - return - } - if (this.onClick) { - this.onClick(this) - } - } + /** + * invokes .onClick(), but only if it is clickable and non-null. + */ + public click() { + if (!this.clickable()) { + return + } + if (this.onClick && this.onClick != null) { + this.onClick(this) + } + } - public draw() { - super.draw() - - if (this.dynamicBoundaryColorsOn) { - const boundaryColour = (this.selected && this.pressable) ? 7 : this.boundaryColor - - for (let dist = 1; dist <= 3; dist++) { - Screen.outlineBoundsXfrm( - this.xfrm, - this.icon.bounds, - dist, - boundaryColour - ) - } - } + public draw() { + super.draw() + + // Draw a boundary colour if requested, this allows you to change a buttons border color based upon whether or not it has been clicked. + if (this.dynamicBoundaryColorsOn) { + const boundaryColour = (this.selected && this.pressable) ? 7 : this.boundaryColor // 7 = green + + // Draw the outline multiple times for a thicker border. + // TODO: optimise this, and the underlying outlineBoundsXfrm, outlineBoundsXfrm invokes drawLine between 4 and 8 times each! + for (let dist = 1; dist <= 3; dist++) { + Screen.outlineBoundsXfrm( + this.xfrm, + this.icon.bounds, + dist, + boundaryColour + ) } + } } + } } diff --git a/component.ts b/component.ts index 287b2af9..6c7e5cec 100644 --- a/component.ts +++ b/component.ts @@ -1,5 +1,4 @@ namespace user_interface_base { - export interface IComponent { update: () => void draw: () => void diff --git a/coreAssets.ts b/coreAssets.ts index c5d8dfb2..91aff9d9 100644 --- a/coreAssets.ts +++ b/coreAssets.ts @@ -1,11 +1,27 @@ namespace user_interface_base { let extraImage: Bitmap = null + /** + * This is used by the webapp. Ignore otherwise. + */ //% shim=TD_NOOP function extraSamples(name: string) { } + + /** + * This contains a number of assets that are shared by all Microbit apps. + * Simply invoke icons.get("compass") to get a bitmap. + * If your program does not use an icon it will be tree-shaken from your program. + * So binary size should be minimised. + * + * The argument nullIfMissing is false by default meaning the icondb.MISSING icon is returned. + * + * If you are adding your own assets we recommend making your own get function in your own namespace, + * that checks for your bitmap names, and invokes this function if it cannot find them. + * See MicroData/assets.ts as an example. + */ export class icons { public static get(name: string, nullIfMissing = false): Bitmap { // editor icons diff --git a/cursor.ts b/cursor.ts index 4770bb16..9414b5d8 100644 --- a/cursor.ts +++ b/cursor.ts @@ -1,173 +1,226 @@ namespace user_interface_base { - import Affine = user_interface_base.Affine - import Bounds = user_interface_base.Bounds - import Screen = user_interface_base.Screen - import IPlaceable = user_interface_base.IPlaceable - import Vec2 = user_interface_base.Vec2 - import IComponent = user_interface_base.IComponent - import Button = user_interface_base.Button + import Affine = user_interface_base.Affine + import Bounds = user_interface_base.Bounds + import Screen = user_interface_base.Screen + import IPlaceable = user_interface_base.IPlaceable + import Vec2 = user_interface_base.Vec2 + import IComponent = user_interface_base.IComponent + import Button = user_interface_base.Button + + + //---------------------------------------------------------------------------------------------------------------- + // This is file is responsible for the Cursor, which is used by the Navigator to show which + // buttons are highlighted over and to activate button.onClick() when the A button is pressed. + // There is only one Cursor per Navigator, one Navigator per CursorScene, and one CursorScene + // on the Screen at a time. The way the cursor moves over a row or grid of buttons is controlled by the Navigator. + // The Cursor can change colour depending on a button state if you wish, see button.dynamicBoundaryColorsOn + // + // The Cursor has a reference to the Navigator which owns it, so that they can call each other. + // The Cursor is also responsible for getting the (optional) text beneath a button and rendering it. + //---------------------------------------------------------------------------------------------------------------- + + + /** + * See .setOutlineColour() + */ + const DEFAULT_CURSOR_OUTLINE_COLOUR: number = 9 // This is light blue. + + export type CursorCancelHandler = () => void + + export enum CursorDir { + Up, + Down, + Left, + Right, + Back, + } + + export interface CursorState { + navigator: INavigator + pos: Vec2 + ariaId: string + size: Bounds + } + + export class Cursor implements IComponent, IPlaceable { + xfrm: Affine + /** The cursor will call navigator.move() as appropriate. */ + navigator: INavigator + cancelHandlerStack: CursorCancelHandler[] + moveStartMs: number + moveDest: Vec2 + ariaPos: Vec2 + ariaId: string + size: Bounds + borderThickness: number + visible = true + + resetOutlineColourOnMove = false + private cursorOutlineColour: number + + constructor() { + this.xfrm = new Affine() + this.cancelHandlerStack = [] + this.moveDest = new Vec2() + this.borderThickness = 3 + this.setSize() + + this.cursorOutlineColour = DEFAULT_CURSOR_OUTLINE_COLOUR + } + + + /** + * Mutates outlineColour, ariaContents and size as neccessary. + * Used by CursorScene. + */ + public moveTo(pos: Vec2, ariaId: string, sizeHint: Bounds) { + if (this.resetOutlineColourOnMove) + this.setOutlineColour(DEFAULT_CURSOR_OUTLINE_COLOUR) + + this.setSize(sizeHint) + this.moveDest.copyFrom(pos) + this.moveStartMs = control.millis() + this.setAriaContent(ariaId) + } + + /** + * Fetch the text that goes beneath the button. + */ + public setAriaContent(ariaId: string, ariaPos: Vec2 = null) { + this.ariaId = ariaId || "" + this.ariaPos = ariaPos + } + + /** + * Alternative to moveTo. + * Used by CursorScene. + */ + public snapTo(x: number, y: number, ariaId: string, sizeHint: Bounds) { + this.setSize( + sizeHint || + new Bounds({ left: 0, top: 0, width: 16, height: 16 }), + ) + this.moveDest.x = this.xfrm.localPos.x = x + this.moveDest.y = this.xfrm.localPos.y = y + this.setAriaContent(ariaId) + } + + public setSize(size?: Bounds) { + size = + size || new Bounds({ left: 0, top: 0, width: 16, height: 16 }) + if (this.size) this.size.copyFrom(size) + else this.size = size.clone() + } + + + /** + * Light blue by default. + */ + public setOutlineColour(colour: number = 9) { + // 9 is the DEFAULT_CURSOR_OUTLINE_COLOUR, which is light blue. + this.cursorOutlineColour = colour + } + + + /** + * Gets the state of the Cursor. + * Used by picker.ts + */ + public saveState(): CursorState { + return { + navigator: this.navigator, + pos: this.xfrm.localPos.clone(), + ariaId: this.ariaId, + size: this.size.clone(), + } + } + + /** + * Rebuild the cursor from a previously fetched state. + * see .saveState(). + * Used by picker.ts + */ + public restoreState(state: CursorState) { + this.navigator = state.navigator + this.xfrm.localPos.copyFrom(state.pos) + this.moveDest.copyFrom(state.pos) + this.ariaId = state.ariaId + this.size.copyFrom(state.size) + } + + + /** + * Tells the navigator to .move() + */ + public move(dir: CursorDir): Button { + return this.navigator.move(dir) + } + + + /** + * Uses the navigator to find the hovered button, invokes it. + */ + public click(): boolean { + let target = this.navigator.getCurrent() //.sort((a, b) => a.z - b.z); + if (target && target.clickable()) { + target.toggleSelected() + target.click() + profile() + return true + } + return false + } + + public cancel(): boolean { + if (this.cancelHandlerStack.length) { + this.cancelHandlerStack[this.cancelHandlerStack.length - 1]() + return true + } + return false + } /** - * See .setOutlineColour() - */ - const DEFAULT_CURSOR_OUTLINE_COLOUR = 9 - - export type CursorCancelHandler = () => void - - export enum CursorDir { - Up, - Down, - Left, - Right, - Back, + * How many times should the border around the button be drawn? + */ + public setBorderThickness(thickness: number) { + this.borderThickness = thickness } - export interface CursorState { - navigator: INavigator - pos: Vec2 - ariaId: string - size: Bounds + update() { + this.xfrm.localPos.copyFrom(this.moveDest) } - export class Cursor implements IComponent, IPlaceable { - xfrm: Affine - navigator: INavigator - cancelHandlerStack: CursorCancelHandler[] - moveStartMs: number - moveDest: Vec2 - ariaPos: Vec2 - ariaId: string - size: Bounds - borderThickness: number - visible = true - - resetOutlineColourOnMove = false - private cursorOutlineColour: number - - constructor() { - this.xfrm = new Affine() - this.cancelHandlerStack = [] - this.moveDest = new Vec2() - this.borderThickness = 3 - this.setSize() - - this.cursorOutlineColour = DEFAULT_CURSOR_OUTLINE_COLOUR - } - - public moveTo(pos: Vec2, ariaId: string, sizeHint: Bounds) { - if (this.resetOutlineColourOnMove) - this.setOutlineColour(DEFAULT_CURSOR_OUTLINE_COLOUR) - - this.setSize(sizeHint) - this.moveDest.copyFrom(pos) - this.moveStartMs = control.millis() - this.setAriaContent(ariaId) - } - - public setAriaContent(ariaId: string, ariaPos: Vec2 = null) { - this.ariaId = ariaId || "" - this.ariaPos = ariaPos - } - - public snapTo(x: number, y: number, ariaId: string, sizeHint: Bounds) { - this.setSize( - sizeHint || - new Bounds({ left: 0, top: 0, width: 16, height: 16 }), - ) - this.moveDest.x = this.xfrm.localPos.x = x - this.moveDest.y = this.xfrm.localPos.y = y - this.setAriaContent(ariaId) - } - - public setSize(size?: Bounds) { - size = - size || new Bounds({ left: 0, top: 0, width: 16, height: 16 }) - if (this.size) this.size.copyFrom(size) - else this.size = size.clone() - } - - public setOutlineColour(colour: number = 9) { - // 9 is the DEFAULT_CURSOR_OUTLINE_COLOUR - this.cursorOutlineColour = colour - } - - public saveState(): CursorState { - return { - navigator: this.navigator, - pos: this.xfrm.localPos.clone(), - ariaId: this.ariaId, - size: this.size.clone(), - } - } - - public restoreState(state: CursorState) { - this.navigator = state.navigator - this.xfrm.localPos.copyFrom(state.pos) - this.moveDest.copyFrom(state.pos) - this.ariaId = state.ariaId - this.size.copyFrom(state.size) - } - - public move(dir: CursorDir): Button { - return this.navigator.move(dir) - } - - public click(): boolean { - let target = this.navigator.getCurrent() //.sort((a, b) => a.z - b.z); - if (target && target.clickable()) { - target.toggleSelected() - target.click() - profile() - return true - } - return false - } - - public cancel(): boolean { - if (this.cancelHandlerStack.length) { - this.cancelHandlerStack[this.cancelHandlerStack.length - 1]() - return true - } - return false - } - - public setBorderThickness(thickness: number) { - this.borderThickness = thickness - } - - update() { - this.xfrm.localPos.copyFrom(this.moveDest) - } - - draw() { - if (!this.visible) return - - for (let dist = 1; dist <= this.borderThickness; dist++) { - Screen.outlineBoundsXfrm( - this.xfrm, - this.size, - dist, - this.cursorOutlineColour, - ) - } - - const text = accessibility.ariaToTooltip(this.ariaId) - if (text) { - const pos = this.ariaPos || this.xfrm.localPos - const n = text.length - const w = font.charWidth * n - const h = font.charHeight - const x = Math.max( - Screen.LEFT_EDGE + 1, - Math.min(Screen.RIGHT_EDGE - 1 - w, pos.x - (w >> 1)), - ) - const y = Math.min( - pos.y + (this.size.width >> 1) + (font.charHeight >> 1) + 1, - Screen.BOTTOM_EDGE - 1 - font.charHeight, - ) - Screen.fillRect(x - 1, y - 1, w + 1, h + 2, 15) - Screen.print(text, x, y, 1, font) - } - } + draw() { + if (!this.visible) return + + // Draw the outline multiple times for a thicker border. + // TODO: optimise this, and the underlying outlineBoundsXfrm, outlineBoundsXfrm invokes drawLine between 4 and 8 times each! + for (let dist = 1; dist <= this.borderThickness; dist++) { + Screen.outlineBoundsXfrm( + this.xfrm, + this.size, + dist, + this.cursorOutlineColour, + ) + } + + const text = accessibility.ariaToTooltip(this.ariaId) + if (text) { + const pos = this.ariaPos || this.xfrm.localPos + const n = text.length + const w = font.charWidth * n + const h = font.charHeight + const x = Math.max( + Screen.LEFT_EDGE + 1, + Math.min(Screen.RIGHT_EDGE - 1 - w, pos.x - (w >> 1)), + ) + const y = Math.min( + pos.y + (this.size.width >> 1) + (font.charHeight >> 1) + 1, + Screen.BOTTOM_EDGE - 1 - font.charHeight, + ) + Screen.fillRect(x - 1, y - 1, w + 1, h + 2, 15) + Screen.print(text, x, y, 1, font) + } } + } } diff --git a/cursorscene.ts b/cursorscene.ts index b2addbbe..4e5b0348 100644 --- a/cursorscene.ts +++ b/cursorscene.ts @@ -1,227 +1,257 @@ namespace user_interface_base { - import Screen = user_interface_base.Screen - import Scene = user_interface_base.Scene + import Screen = user_interface_base.Screen + import Scene = user_interface_base.Scene + + /** + * Used to control the flow between scenes, + * The SensorSelect scene is used to set the sensors before the RecordData, DistributedLogging and LiveDataViewer scenes + * This enum may be passed to the constructors of these scenes so that they can dynamically control this flow. + * TODO: REMOVE THIS!! + */ + export enum CursorSceneEnum { + LiveDataViewer, + SensorSelect, + RecordingConfigSelect, + RecordData, + DistributedLogging + } + + + /** + * Top-level abstraction that is rendered on the screen. + * It owns a Navigator, which has an group of organised buttons (such as a row or grid). + * a Cursor which shows which button is currently selected, and a picker. + * Defaults Navigator to NULL if it is not passed. + */ + export class CursorScene extends Scene { + navigator: INavigator + public cursor: Cursor + public picker: Picker + + constructor(app: AppInterface, navigator?: INavigator) { + super(app, "scene") + this.backgroundColor = 11 + + if (navigator) + this.navigator = navigator + else + this.navigator = null + } + + public moveCursor(dir: CursorDir) { + try { + this.moveTo(this.cursor.move(dir)) + } catch (e) { + if (dir === CursorDir.Up && e.kind === BACK_BUTTON_ERROR_KIND) + this.back() + else if ( + dir === CursorDir.Down && + e.kind === FORWARD_BUTTON_ERROR_KIND + ) + return + else throw e + } + } + + protected moveTo(target: Button) { + if (!target) return + this.cursor.moveTo( + target.xfrm.worldPos, + target.ariaId, + target.bounds + ) + } + + + /** + * Setup the controller buttons. + * If you wish to override what the controller buttons do, then pass controlSetupFn. + * overrides parent method, still invokes it. + */ + startup(controlSetupFn?: () => void) { + super.startup() + if (controlSetupFn != null) { + controlSetupFn(); + } else { + control.onEvent( + ControllerButtonEvent.Pressed, + controller.right.id, + () => this.moveCursor(CursorDir.Right) + ) + control.onEvent( + ControllerButtonEvent.Pressed, + controller.up.id, + () => this.moveCursor(CursorDir.Up) + ) + control.onEvent( + ControllerButtonEvent.Pressed, + controller.down.id, + () => this.moveCursor(CursorDir.Down) + ) + control.onEvent( + ControllerButtonEvent.Pressed, + controller.left.id, + () => this.moveCursor(CursorDir.Left) + ) + + // click + const click = () => this.cursor.click() + control.onEvent( + ControllerButtonEvent.Pressed, + controller.A.id, + click + ) + control.onEvent( + ControllerButtonEvent.Pressed, + controller.A.id + keymap.PLAYER_OFFSET, + click + ) + control.onEvent( + ControllerButtonEvent.Pressed, + controller.B.id, + () => this.back() + ) + } + this.cursor = new Cursor() + this.picker = new Picker(this.cursor) + if (this.navigator == null) + this.navigator = new RowNavigator() + this.cursor.navigator = this.navigator + } /** - * Used to control the flow between scenes, - * The SensorSelect scene is used to set the sensors before the RecordData, DistributedLogging and LiveDataViewer scenes - * This enum may be passed to the constructors of these scenes so that they can dynamically control this flow. - */ - export enum CursorSceneEnum { - LiveDataViewer, - SensorSelect, - RecordingConfigSelect, - RecordData, - DistributedLogging + * What this does is specific to the Navigator. + * The RowNavigator will make the Cursor hover over the first Button. + */ + back() { + if (!this.cursor.cancel()) this.moveCursor(CursorDir.Back) } - export class CursorScene extends Scene { - navigator: INavigator - public cursor: Cursor - public picker: Picker - - constructor(app: AppInterface, navigator?: INavigator) { - super(app, "scene") - this.backgroundColor = 11 - - if (navigator) - this.navigator = navigator - else - this.navigator = null - } - - protected moveCursor(dir: CursorDir) { - try { - this.moveTo(this.cursor.move(dir)) - } catch (e) { - if (dir === CursorDir.Up && e.kind === BACK_BUTTON_ERROR_KIND) - this.back() - else if ( - dir === CursorDir.Down && - e.kind === FORWARD_BUTTON_ERROR_KIND - ) - return - else throw e - } - } - - protected moveTo(target: Button) { - if (!target) return - this.cursor.moveTo( - target.xfrm.worldPos, - target.ariaId, - target.bounds - ) - } - - /* override */ startup() { - super.startup() - control.onEvent( - ControllerButtonEvent.Pressed, - controller.right.id, - () => this.moveCursor(CursorDir.Right) - ) - control.onEvent( - ControllerButtonEvent.Pressed, - controller.up.id, - () => this.moveCursor(CursorDir.Up) - ) - control.onEvent( - ControllerButtonEvent.Pressed, - controller.down.id, - () => this.moveCursor(CursorDir.Down) - ) - control.onEvent( - ControllerButtonEvent.Pressed, - controller.left.id, - () => this.moveCursor(CursorDir.Left) - ) - - // click - const click = () => this.cursor.click() - control.onEvent( - ControllerButtonEvent.Pressed, - controller.A.id, - click - ) - control.onEvent( - ControllerButtonEvent.Pressed, - controller.A.id + keymap.PLAYER_OFFSET, - click - ) - control.onEvent( - ControllerButtonEvent.Pressed, - controller.B.id, - () => this.back() - ) - - this.cursor = new Cursor() - this.picker = new Picker(this.cursor) - if (this.navigator == null) - this.navigator = new RowNavigator() - this.cursor.navigator = this.navigator - } - - back() { - if (!this.cursor.cancel()) this.moveCursor(CursorDir.Back) - } - - protected handleClick(x: number, y: number) { - const target = this.cursor.navigator.screenToButton( - x - Screen.HALF_WIDTH, - y - Screen.HALF_HEIGHT - ) - if (target) { - this.moveTo(target) - target.click() - } else if (this.picker.visible) { - this.picker.hide() - } - } - - protected handleMove(x: number, y: number) { - const btn = this.cursor.navigator.screenToButton( - x - Screen.HALF_WIDTH, - y - Screen.HALF_HEIGHT - ) - if (btn) { - const w = btn.xfrm.worldPos - this.cursor.snapTo(w.x, w.y, btn.ariaId, btn.bounds) - btn.reportAria(true) - } - } + + protected handleClick(x: number, y: number) { + const target = this.cursor.navigator.screenToButton( + x - Screen.HALF_WIDTH, + y - Screen.HALF_HEIGHT + ) + if (target) { + this.moveTo(target) + target.click() + } else if (this.picker.visible) { + this.picker.hide() + } + } + + protected handleMove(x: number, y: number) { + const btn = this.cursor.navigator.screenToButton( + x - Screen.HALF_WIDTH, + y - Screen.HALF_HEIGHT + ) + if (btn) { + const w = btn.xfrm.worldPos + this.cursor.snapTo(w.x, w.y, btn.ariaId, btn.bounds) + btn.reportAria(true) + } + } /* override */ shutdown() { - this.navigator.clear() - } + this.navigator.clear() + } /* override */ activate() { - super.activate() - const btn = this.navigator.initialCursor(0, 0) - if (btn) { - const w = btn.xfrm.worldPos - this.cursor.snapTo(w.x, w.y, btn.ariaId, btn.bounds) - btn.reportAria(true) - } - } + super.activate() + const btn = this.navigator.initialCursor(0, 0) + if (btn) { + const w = btn.xfrm.worldPos + this.cursor.snapTo(w.x, w.y, btn.ariaId, btn.bounds) + btn.reportAria(true) + } + } /* override */ update() { - this.cursor.update() - } + this.cursor.update() + } /* override */ draw() { - this.picker.draw() - this.cursor.draw() - } + this.picker.draw() + this.cursor.draw() } + } + + + /** + * Ovverides the B button on the controller to go invoke a passed function. + * That passed function is normally {this.app.popScene(); this.app.pushScene(new ...); + * Defaults to RowNavigator if not passsed. + */ + export class CursorSceneWithPriorPage extends CursorScene { + private goBack1PageFn: () => void; + + constructor(app: AppInterface, goBack1PageFn: () => void, navigator?: INavigator) { + super(app) + this.backgroundColor = 11 + + if (navigator) + this.navigator = navigator + else + this.navigator = null + this.goBack1PageFn = goBack1PageFn + } + + /* override */ startup(controlSetupFn?: () => void) { + if (controlSetupFn != null) { + controlSetupFn(); + } else { + control.onEvent( + ControllerButtonEvent.Pressed, + controller.right.id, + () => this.moveCursor(CursorDir.Right) + ) + control.onEvent( + ControllerButtonEvent.Pressed, + controller.up.id, + () => this.moveCursor(CursorDir.Up) + ) + control.onEvent( + ControllerButtonEvent.Pressed, + controller.down.id, + () => this.moveCursor(CursorDir.Down) + ) + control.onEvent( + ControllerButtonEvent.Pressed, + controller.left.id, + () => this.moveCursor(CursorDir.Left) + ) + + // click + const click = () => this.cursor.click() + control.onEvent( + ControllerButtonEvent.Pressed, + controller.A.id, + click + ) + control.onEvent( + ControllerButtonEvent.Pressed, + controller.A.id + keymap.PLAYER_OFFSET, + click + ) + control.onEvent( + ControllerButtonEvent.Pressed, + controller.B.id, + () => this.back() + ) + control.onEvent( + ControllerButtonEvent.Pressed, + controller.B.id, + () => this.goBack1PageFn() + ) + } + this.cursor = new Cursor() + this.picker = new Picker(this.cursor) - export class CursorSceneWithPriorPage extends CursorScene { - private goBack1PageFn: () => void; - - constructor(app: AppInterface, goBack1PageFn: () => void, navigator?: INavigator) { - super(app) - this.backgroundColor = 11 - - if (navigator) - this.navigator = navigator - else - this.navigator = null - this.goBack1PageFn = goBack1PageFn - } - - /* override */ startup() { - control.onEvent( - ControllerButtonEvent.Pressed, - controller.right.id, - () => this.moveCursor(CursorDir.Right) - ) - control.onEvent( - ControllerButtonEvent.Pressed, - controller.up.id, - () => this.moveCursor(CursorDir.Up) - ) - control.onEvent( - ControllerButtonEvent.Pressed, - controller.down.id, - () => this.moveCursor(CursorDir.Down) - ) - control.onEvent( - ControllerButtonEvent.Pressed, - controller.left.id, - () => this.moveCursor(CursorDir.Left) - ) - - // click - const click = () => this.cursor.click() - control.onEvent( - ControllerButtonEvent.Pressed, - controller.A.id, - click - ) - control.onEvent( - ControllerButtonEvent.Pressed, - controller.A.id + keymap.PLAYER_OFFSET, - click - ) - control.onEvent( - ControllerButtonEvent.Pressed, - controller.B.id, - () => this.back() - ) - - control.onEvent( - ControllerButtonEvent.Pressed, - controller.B.id, - () => this.goBack1PageFn() - ) - - this.cursor = new Cursor() - this.picker = new Picker(this.cursor) - - if (this.navigator == null) - this.navigator = new RowNavigator() - this.cursor.navigator = this.navigator - } + if (this.navigator == null) + this.navigator = new RowNavigator() + this.cursor.navigator = this.navigator } + } } diff --git a/math.ts b/math.ts index 586a33fb..a1ef9879 100644 --- a/math.ts +++ b/math.ts @@ -1,4 +1,7 @@ namespace user_interface_base { + /** + * Used by Affine and others, useful for operations on Sprites. + */ export class Vec2 { public get x() { return this.x_ diff --git a/navigator.ts b/navigator.ts index 157a3277..68891363 100644 --- a/navigator.ts +++ b/navigator.ts @@ -1,9 +1,21 @@ namespace user_interface_base { + /** + * A Navigator is a wrapper over a group of buttons. + * It is owned by a CursorScene. When you press a physical button on a controller, + * the cursor will invoke methods on the Navigator and Cursor to change the GUI. + * Dynamically adding or removing buttons is supported. + * The main types of Navigator are Row and Grid. + * + * You do not need to hold references to buttons locally, they can all be held in here. + * Ensure that this.navigator.drawComponents() is invoked in your CursorScene.draw() + */ export interface INavigator { clear: () => void setBtns: (btns: Button[][]) => void addRow: (btns: Button[]) => void addCol: (btns: Button[]) => void + getRow: () => number + getCol: () => number move: (dir: CursorDir) => Button getCurrent: () => Button screenToButton: (x: number, y: number) => Button @@ -21,14 +33,24 @@ namespace user_interface_base { } } - // ragged rows of buttons + + /** + * A ragged rows of buttons + * When B is pressed the CursorScene, but not the CursorSceneWithPriorPage, + * will tell this Navigator to show the Cursor as hovering over the first button again. + * You can have multiple rows (addRow() is supported) if you wish, + * But the GridNavigator is recommended for that usecase. + */ export class RowNavigator implements INavigator { + /** This can be used to support multiple Rows, but this is not recommended. */ protected buttonGroups: Button[][] protected row: number protected col: number constructor() { this.buttonGroups = [] + this.row = 0; + this.col = 0; } public clear() { @@ -38,6 +60,10 @@ namespace user_interface_base { public getRow() { return this.row } + + public getCol() { + return this.col; + } public setBtns(btns: Button[][]) { this.buttonGroups = btns @@ -83,6 +109,9 @@ namespace user_interface_base { return undefined } + /** + * Invoked by the CursorScene. + */ public move(dir: CursorDir) { this.makeGood() switch (dir) { @@ -153,7 +182,10 @@ namespace user_interface_base { public getCurrent(): Button { return this.buttonGroups[this.row][this.col] } - + + /** + * Helper function to ensure .row and .col are not out of bounds. + */ protected makeGood() { if (this.row >= this.buttonGroups.length) this.row = this.buttonGroups.length - 1 @@ -161,6 +193,10 @@ namespace user_interface_base { this.col = this.buttonGroups[this.row].length - 1 } + + /** + * Ensure that the default cursor position is inbounds. + */ public initialCursor(row: number = 0, col: number = 0) { const rows = this.buttonGroups.length while (row < 0) row = (row + rows) % rows @@ -172,6 +208,10 @@ namespace user_interface_base { } } + + /** + * A Navigator for hetrogenous rows and columns. + */ export class GridNavigator extends RowNavigator { private height: number; private widths: number[]; @@ -189,6 +229,14 @@ namespace user_interface_base { } } + public getRow() { + return this.row + } + + public getCol() { + return this.col; + } + public setBtns(btns: Button[][]) { this.buttonGroups = btns this.widths = btns.map(row => row.length) @@ -279,15 +327,19 @@ namespace user_interface_base { } - // mostly a matrix, except for last row, which may be ragged - // also supports delete button - // add support for aria + /** + * Mostly a matrix, except for last row, which may be ragged + * Supports delete button + */ export class PickerNavigator implements INavigator { protected deleteButton: Button protected row: number protected col: number - constructor(private picker: Picker) { } + constructor(private picker: Picker) { + this.row = 0; + this.col = 0; + } private get width() { return this.picker.width @@ -300,6 +352,14 @@ namespace user_interface_base { return !!this.deleteButton } + public getRow() { + return this.row + } + + public getCol() { + return this.col; + } + public setBtns(btns: Button[][]) { } public addRow(btns: Button[]) { } public addCol(btns: Button[]) { } diff --git a/options.ts b/options.ts index 4db5cb45..b41e9ecc 100644 --- a/options.ts +++ b/options.ts @@ -2,6 +2,7 @@ namespace user_interface_base { export class Options { public static fps = false public static profiling = false + /** See CursorScene.startup(); */ public static menuProfiling = false // heap-dump on MENU press } diff --git a/pxt.json b/pxt.json index 2d0f073b..aa36514a 100644 --- a/pxt.json +++ b/pxt.json @@ -1,6 +1,6 @@ { "name": "user-interface-base", - "version": "0.0.19", + "version": "0.0.26", "files": [ "affine.ts", "bounds.ts", diff --git a/scene.ts b/scene.ts index 94f4b684..b4cd7944 100644 --- a/scene.ts +++ b/scene.ts @@ -4,6 +4,12 @@ namespace user_interface_base { const RENDER_PRIORITY = 30 const SCREEN_PRIORITY = 100 + /** + * Top-level abstraction drawn to the screen. + * Extended by CursorScene when you want to have a GUI with Button support. + * Useful if you: don't want buttons, aren't making a GUI, or to make a more complex GUI. + * CursorScene overwrites a number of these abstract methods. + */ export abstract class Scene implements IComponent { private xfrm_: Affine private color_: number @@ -26,7 +32,7 @@ namespace user_interface_base { this.color_ = 12 } - /* abstract */ startup() { + /* abstract */ startup(controlSetupFn?: () => {}) { if (Options.menuProfiling) { context.onEvent( ControllerButtonEvent.Pressed, @@ -103,6 +109,15 @@ namespace user_interface_base { } } + + /** + * This is an wrapper around a stack of scenes. + * You can .push() and .pop() scenes to navigator your application. + * In a microbit App this SceneManager is setup in app.ts, + * which has a wrapper around SceneManager methods. + * The app object is normally passed as a reference between scenes, + * so that they can access this SceneManager. + */ export class SceneManager { scenes: Scene[] diff --git a/screen.ts b/screen.ts index 9c9f7a3f..4287288b 100644 --- a/screen.ts +++ b/screen.ts @@ -1,357 +1,363 @@ - namespace user_interface_base { - export class Screen { - private static image_: Bitmap +namespace user_interface_base { + /** + * This is a wrapper around a Bitmap; which represents the screen. + * Abstractions like Button, Cursor and Scene invoke this object. + * It has static methods so you are also able to use it anywhere like so: + * Screen.fill(6); + */ + export class Screen { + private static image_: Bitmap - public static WIDTH = screen().width - public static HEIGHT = screen().height - public static HALF_WIDTH = screen().width >> 1 - public static HALF_HEIGHT = screen().height >> 1 - public static LEFT_EDGE = -Screen.HALF_WIDTH - public static RIGHT_EDGE = Screen.HALF_WIDTH - public static TOP_EDGE = -Screen.HALF_HEIGHT - public static BOTTOM_EDGE = Screen.HALF_HEIGHT - public static BOUNDS = new Bounds({ - left: Screen.LEFT_EDGE, - top: Screen.TOP_EDGE, - width: Screen.WIDTH, - height: Screen.HEIGHT, - }) + public static WIDTH = screen().width + public static HEIGHT = screen().height + public static HALF_WIDTH = screen().width >> 1 + public static HALF_HEIGHT = screen().height >> 1 + public static LEFT_EDGE = -Screen.HALF_WIDTH + public static RIGHT_EDGE = Screen.HALF_WIDTH + public static TOP_EDGE = -Screen.HALF_HEIGHT + public static BOTTOM_EDGE = Screen.HALF_HEIGHT + public static BOUNDS = new Bounds({ + left: Screen.LEFT_EDGE, + top: Screen.TOP_EDGE, + width: Screen.WIDTH, + height: Screen.HEIGHT, + }) - private static updateBounds() { - Screen.WIDTH = Screen.image_.width - Screen.HEIGHT = Screen.image_.height - Screen.HALF_WIDTH = Screen.WIDTH >> 1 - Screen.HALF_HEIGHT = Screen.HEIGHT >> 1 - Screen.LEFT_EDGE = -Screen.HALF_WIDTH - Screen.RIGHT_EDGE = Screen.HALF_WIDTH - Screen.TOP_EDGE = -Screen.HALF_HEIGHT - Screen.BOTTOM_EDGE = Screen.HALF_HEIGHT - Screen.BOUNDS = new Bounds({ - left: Screen.LEFT_EDGE, - top: Screen.TOP_EDGE, - width: Screen.WIDTH, - height: Screen.HEIGHT, - }) - } + private static updateBounds() { + Screen.WIDTH = Screen.image_.width + Screen.HEIGHT = Screen.image_.height + Screen.HALF_WIDTH = Screen.WIDTH >> 1 + Screen.HALF_HEIGHT = Screen.HEIGHT >> 1 + Screen.LEFT_EDGE = -Screen.HALF_WIDTH + Screen.RIGHT_EDGE = Screen.HALF_WIDTH + Screen.TOP_EDGE = -Screen.HALF_HEIGHT + Screen.BOTTOM_EDGE = Screen.HALF_HEIGHT + Screen.BOUNDS = new Bounds({ + left: Screen.LEFT_EDGE, + top: Screen.TOP_EDGE, + width: Screen.WIDTH, + height: Screen.HEIGHT, + }) + } - public static x(v: number) { - return v + Screen.HALF_WIDTH - } - public static y(v: number) { - return v + Screen.HALF_HEIGHT - } - public static pos(v: Vec2) { - return new Vec2(Screen.x(v.x), Screen.y(v.y)) - } - public static get image(): Bitmap { - if (!Screen.image_) { - Screen.image_ = screen() - Screen.updateBounds() - } - return Screen.image_ - } - public static resetScreenImage() { - Screen.image_ = screen() - Screen.updateBounds() - } + public static x(v: number) { + return v + Screen.HALF_WIDTH + } + public static y(v: number) { + return v + Screen.HALF_HEIGHT + } + public static pos(v: Vec2) { + return new Vec2(Screen.x(v.x), Screen.y(v.y)) + } + public static get image(): Bitmap { + if (!Screen.image_) { + Screen.image_ = screen() + Screen.updateBounds() + } + return Screen.image_ + } + public static resetScreenImage() { + Screen.image_ = screen() + Screen.updateBounds() + } - public static setImageSize(width: number, height: number) { - Screen.image_ = bitmaps.create(width, height) - Screen.updateBounds() - } + public static setImageSize(width: number, height: number) { + Screen.image_ = bitmaps.create(width, height) + Screen.updateBounds() + } - public static drawTransparentImage(from: Bitmap, x: number, y: number) { - Screen.image.drawTransparentBitmap(from, Screen.x(x), Screen.y(y)) - } + public static drawTransparentImage(from: Bitmap, x: number, y: number) { + Screen.image.drawTransparentBitmap(from, Screen.x(x), Screen.y(y)) + } - public static drawTransparentImageXfrm( - xfrm: Affine, - from: Bitmap, - x: number, - y: number - ) { - const w = xfrm.worldPos - Screen.image.drawTransparentBitmap( - from, - Screen.x(x + w.x), - Screen.y(y + w.y) - ) - } + public static drawTransparentImageXfrm( + xfrm: Affine, + from: Bitmap, + x: number, + y: number + ) { + const w = xfrm.worldPos + Screen.image.drawTransparentBitmap( + from, + Screen.x(x + w.x), + Screen.y(y + w.y) + ) + } - public static drawLine( - x0: number, - y0: number, - x1: number, - y1: number, - c: number - ) { - Screen.image.drawLine( - Screen.x(x0), - Screen.y(y0), - Screen.x(x1), - Screen.y(y1), - c - ) - } + public static drawLine( + x0: number, + y0: number, + x1: number, + y1: number, + c: number + ) { + Screen.image.drawLine( + Screen.x(x0), + Screen.y(y0), + Screen.x(x1), + Screen.y(y1), + c + ) + } - public static drawLineXfrm( - xfrm: Affine, - x0: number, - y0: number, - x1: number, - y1: number, - c: number - ) { - const w = xfrm.worldPos - Screen.drawLine(x0 + w.x, y0 + w.y, x1 + w.x, y1 + w.y, c) - } + public static drawLineXfrm( + xfrm: Affine, + x0: number, + y0: number, + x1: number, + y1: number, + c: number + ) { + const w = xfrm.worldPos + Screen.drawLine(x0 + w.x, y0 + w.y, x1 + w.x, y1 + w.y, c) + } - public static drawLineShaded( - x0: number, - y0: number, - x1: number, - y1: number, - shader: (x: number, y: number) => number - ) { - let sx0 = Screen.x(x0) - let sy0 = Screen.y(y0) - let sx1 = Screen.x(x1) - let sy1 = Screen.y(y1) + public static drawLineShaded( + x0: number, + y0: number, + x1: number, + y1: number, + shader: (x: number, y: number) => number + ) { + let sx0 = Screen.x(x0) + let sy0 = Screen.y(y0) + let sx1 = Screen.x(x1) + let sy1 = Screen.y(y1) - for (let x = sx0, tx = x0; x <= sx1; x++, tx++) { - for (let y = sy0, ty = y0; y <= sy1; y++, ty++) { - const c = shader(tx, ty) - if (c) { - Screen.image.setPixel(x, y, c) - } - } - } + for (let x = sx0, tx = x0; x <= sx1; x++, tx++) { + for (let y = sy0, ty = y0; y <= sy1; y++, ty++) { + const c = shader(tx, ty) + if (c) { + Screen.image.setPixel(x, y, c) + } } + } + } - public static drawRect( - x: number, - y: number, - width: number, - height: number, - c: number - ) { - Screen.image.drawRect(Screen.x(x), Screen.y(y), width, height, c) - } + public static drawRect( + x: number, + y: number, + width: number, + height: number, + c: number + ) { + Screen.image.drawRect(Screen.x(x), Screen.y(y), width, height, c) + } - public static drawRectXfrm( - xfrm: Affine, - x: number, - y: number, - width: number, - height: number, - c: number - ) { - const w = xfrm.worldPos - Screen.drawRect(x + w.x, y + w.y, width, height, c) - } + public static drawRectXfrm( + xfrm: Affine, + x: number, + y: number, + width: number, + height: number, + c: number + ) { + const w = xfrm.worldPos + Screen.drawRect(x + w.x, y + w.y, width, height, c) + } - public static fillRect( - x: number, - y: number, - width: number, - height: number, - c: number - ) { - Screen.image.fillRect(Screen.x(x), Screen.y(y), width, height, c) - } + public static fillRect( + x: number, + y: number, + width: number, + height: number, + c: number + ) { + Screen.image.fillRect(Screen.x(x), Screen.y(y), width, height, c) + } - public static fillRectXfrm( - xfrm: Affine, - x: number, - y: number, - width: number, - height: number, - c: number - ) { - const w = xfrm.worldPos - Screen.fillRect(x + w.x, y + w.y, width, height, c) - } + public static fillRectXfrm( + xfrm: Affine, + x: number, + y: number, + width: number, + height: number, + c: number + ) { + const w = xfrm.worldPos + Screen.fillRect(x + w.x, y + w.y, width, height, c) + } - public static fillBoundsXfrm(xfrm: Affine, bounds: Bounds, c: number) { - Screen.fillRectXfrm( - xfrm, - bounds.left, - bounds.top, - bounds.width, - bounds.height, - c - ) - } + public static fillBoundsXfrm(xfrm: Affine, bounds: Bounds, c: number) { + Screen.fillRectXfrm( + xfrm, + bounds.left, + bounds.top, + bounds.width, + bounds.height, + c + ) + } - public static drawBoundsXfrm(xfrm: Affine, bounds: Bounds, c: number) { - Screen.drawRectXfrm( - xfrm, - bounds.left, - bounds.top, - bounds.width, - bounds.height, - c - ) - } + public static drawBoundsXfrm(xfrm: Affine, bounds: Bounds, c: number) { + Screen.drawRectXfrm( + xfrm, + bounds.left, + bounds.top, + bounds.width, + bounds.height, + c + ) + } - // Draws a rounded outline rectangle of the bounds. - public static outlineBoundsXfrm( - xfrm: Affine, - bounds: Bounds, - dist: number, - c: number - ) { - if (!c) return + // Draws a rounded outline rectangle of the bounds. + public static outlineBoundsXfrm( + xfrm: Affine, + bounds: Bounds, + dist: number, + c: number + ) { + if (!c) return - const w = xfrm.worldPos - const left = bounds.left + w.x - const top = bounds.top + w.y - const right = bounds.right + w.x - const bottom = bounds.bottom + w.y + const w = xfrm.worldPos + const left = bounds.left + w.x + const top = bounds.top + w.y + const right = bounds.right + w.x + const bottom = bounds.bottom + w.y - // Left - Screen.drawLine(left - dist, top, left - dist, bottom, c) - // Right - Screen.drawLine(right + dist, top, right + dist, bottom, c) - // Top - Screen.drawLine(left, top - dist, right, top - dist, c) - // Bottom - Screen.drawLine(left, bottom + dist, right, bottom + dist, c) + // Left + Screen.drawLine(left - dist, top, left - dist, bottom, c) + // Right + Screen.drawLine(right + dist, top, right + dist, bottom, c) + // Top + Screen.drawLine(left, top - dist, right, top - dist, c) + // Bottom + Screen.drawLine(left, bottom + dist, right, bottom + dist, c) - // Connect corners - if (dist > 1) { - // Left-Top - Screen.drawLine(left - dist, top, left, top - dist, c) - // Right-Top - Screen.drawLine(right + dist, top, right, top - dist, c) - // Left-Bottom - Screen.drawLine(left - dist, bottom, left, bottom + dist, c) - // Right-Bottom - Screen.drawLine(right + dist, bottom, right, bottom + dist, c) - } - } + // Connect corners + if (dist > 1) { + // Left-Top + Screen.drawLine(left - dist, top, left, top - dist, c) + // Right-Top + Screen.drawLine(right + dist, top, right, top - dist, c) + // Left-Bottom + Screen.drawLine(left - dist, bottom, left, bottom + dist, c) + // Right-Bottom + Screen.drawLine(right + dist, bottom, right, bottom + dist, c) + } + } - // Draws a rounded outline rectangle of the bounds. - public static outlineBoundsXfrm4( - xfrm: Affine, - bounds: Bounds, - dist: number, - colors: { top: number; left: number; right: number; bottom: number } - ) { - // no borders! - if (!colors.top && !colors.left && !colors.right && !colors.bottom) - return + // Draws a rounded outline rectangle of the bounds. + public static outlineBoundsXfrm4( + xfrm: Affine, + bounds: Bounds, + dist: number, + colors: { top: number; left: number; right: number; bottom: number } + ) { + // no borders! + if (!colors.top && !colors.left && !colors.right && !colors.bottom) + return - const w = xfrm.worldPos - const left = bounds.left + w.x - const top = bounds.top + w.y - const right = bounds.right + w.x - const bottom = bounds.bottom + w.y + const w = xfrm.worldPos + const left = bounds.left + w.x + const top = bounds.top + w.y + const right = bounds.right + w.x + const bottom = bounds.bottom + w.y - // Left - if (colors.left) - Screen.drawLine( - left - dist, - top, - left - dist, - bottom, - colors.left - ) - // Right - if (colors.right) - Screen.drawLine( - right + dist, - top, - right + dist, - bottom, - colors.right - ) - // Top - if (colors.top) - Screen.drawLine(left, top - dist, right, top - dist, colors.top) - // Bottom - if (colors.bottom) - Screen.drawLine( - left, - bottom + dist, - right, - bottom + dist, - colors.bottom - ) + // Left + if (colors.left) + Screen.drawLine( + left - dist, + top, + left - dist, + bottom, + colors.left + ) + // Right + if (colors.right) + Screen.drawLine( + right + dist, + top, + right + dist, + bottom, + colors.right + ) + // Top + if (colors.top) + Screen.drawLine(left, top - dist, right, top - dist, colors.top) + // Bottom + if (colors.bottom) + Screen.drawLine( + left, + bottom + dist, + right, + bottom + dist, + colors.bottom + ) - // Connect corners - if (dist > 1) { - // Left-Top - if (colors.left) - Screen.drawLine( - left - dist, - top, - left, - top - dist, - colors.left - ) - // Right-Top - if (colors.right) - Screen.drawLine( - right + dist, - top, - right, - top - dist, - colors.right - ) - // Left-Bottom - if (colors.left) - Screen.drawLine( - left - dist, - bottom, - left, - bottom + dist, - colors.left - ) - // Right-Bottom - if (colors.right) - Screen.drawLine( - right + dist, - bottom, - right, - bottom + dist, - colors.right - ) - } - } + // Connect corners + if (dist > 1) { + // Left-Top + if (colors.left) + Screen.drawLine( + left - dist, + top, + left, + top - dist, + colors.left + ) + // Right-Top + if (colors.right) + Screen.drawLine( + right + dist, + top, + right, + top - dist, + colors.right + ) + // Left-Bottom + if (colors.left) + Screen.drawLine( + left - dist, + bottom, + left, + bottom + dist, + colors.left + ) + // Right-Bottom + if (colors.right) + Screen.drawLine( + right + dist, + bottom, + right, + bottom + dist, + colors.right + ) + } + } - public static setPixel(x: number, y: number, c: number) { - if (c) { - Screen.image.setPixel(Screen.x(x), Screen.y(y), c) - } - } + public static setPixel(x: number, y: number, c: number) { + if (c) { + Screen.image.setPixel(Screen.x(x), Screen.y(y), c) + } + } - public static setPixelXfrm( - xfrm: Affine, - x: number, - y: number, - c: number - ) { - const w = xfrm.worldPos - Screen.setPixel(x + w.x, y + w.y, c) - } + public static setPixelXfrm( + xfrm: Affine, + x: number, + y: number, + c: number + ) { + const w = xfrm.worldPos + Screen.setPixel(x + w.x, y + w.y, c) + } - public static print( - text: string, - x: number, - y: number, - color?: number, - font?: bitmaps.Font, - offsets?: texteffects.TextEffectState[] - ) { - Screen.image.print( - text, - Screen.x(x), - Screen.y(y), - color, - font, - offsets - ) - } + public static print( + text: string, + x: number, + y: number, + color?: number, + font?: bitmaps.Font, + offsets?: texteffects.TextEffectState[] + ) { + Screen.image.print( + text, + Screen.x(x), + Screen.y(y), + color, + font, + offsets + ) } + } } diff --git a/sprite.ts b/sprite.ts index 12821581..ac4c0e68 100644 --- a/sprite.ts +++ b/sprite.ts @@ -1,4 +1,11 @@ namespace user_interface_base { + /** + * This is a wrapper around a Bitmap, + * Notably providing a hitbox. + * It will draw the provided bitmap transparently. + * This means that any black pixels in the bitmap will + * change to be the same colour as whatever is behind the bitmap. + */ export class Sprite implements IComponent, IPlaceable, ISizable { private xfrm_: Affine image: Bitmap diff --git a/tsconfig.json b/tsconfig.json index 1f12644b..4d0a08b5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,4 +12,4 @@ "built/**", "pxt_modules/**/*test.ts" ] -} \ No newline at end of file +}