Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions Source/DotNET/Model/Elements/Panels/Canvas.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Cratis.Scene.Model.Common;

namespace Cratis.Scene.Model.Elements.Panels;

/// <summary>
/// Places its children at absolute coordinates within a coordinate space of its own. A child states where
/// it sits through the <c>Canvas.Left</c>, <c>Canvas.Top</c>, <c>Canvas.Right</c> and <c>Canvas.Bottom</c>
/// keys of its <see cref="SceneElement.Properties"/>, which is how an attached property is carried in a
/// model that has no attached properties of its own.
/// </summary>
/// <remarks>
/// <para>
/// Named for <c>@cratis/components</c>' <c>Canvas</c>, which is the surface a Cratis application actually
/// draws one on. The vocabulary is shared deliberately: a designer placing components on a canvas and the
/// model recording where they landed should not need two words for the same thing.
/// </para>
/// <para>
/// This is a panel in the element tree, and is not the same concept as a
/// <see cref="Layouts.FreeformArrangement"/> - that arranges a <em>layout slot's</em> content, with one
/// placement variant per size class. A canvas places elements inside a single coordinate space, and is
/// what a free-placement design surface serializes to.
/// </para>
/// </remarks>
public record Canvas : Panel
{
/// <summary>
/// Gets the size of the coordinate space children are placed in - the design surface a
/// <c>Canvas.Left</c>/<c>Canvas.Top</c> pair is measured against.
/// </summary>
/// <remarks>
/// Either dimension left unspecified means the renderer decides, exactly as elsewhere. This is also
/// what tells a canvas apart from a plain <see cref="Panel"/>: the model carries no discriminator,
/// so a panel is recognized by the property it alone declares.
/// </remarks>
public Size Extent { get; init; } = new();
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

import { DockPanel, Grid, HorizontalAlignment, Orientation, Panel, StackPanel, VerticalAlignment, Visibility, WrapPanel } from '@cratis/scene.model';
import { isDockPanel, isGrid, isStackPanel, isWrapPanel } from '../index';
import { Canvas, DockPanel, Grid, HorizontalAlignment, Orientation, Panel, StackPanel, VerticalAlignment, Visibility, WrapPanel } from '@cratis/scene.model';
import { isCanvas, isDockPanel, isGrid, isStackPanel, isWrapPanel } from '../index';

const base = {
id: 'panel',
Expand All @@ -21,13 +21,15 @@ const base = {
children: [],
};

const canvas: Canvas = { ...base, extent: { width: 1280, height: 800 } };
const grid: Grid = { ...base, rows: [], columns: [] };
const dock: DockPanel = { ...base, lastChildFill: true };
const stack: StackPanel = { ...base, orientation: Orientation.Vertical, spacing: 0 };
const wrap: WrapPanel = { ...base, orientation: Orientation.Horizontal };
const plain: Panel = { ...base };

describe('when telling the panels apart', () => {
it('should recognize a canvas by its extent', () => isCanvas(canvas).should.be.true);
it('should recognize a grid by its tracks', () => isGrid(grid).should.be.true);
it('should recognize a dock panel by its fill flag', () => isDockPanel(dock).should.be.true);
it('should recognize a stack panel by its spacing', () => isStackPanel(stack).should.be.true);
Expand All @@ -36,7 +38,10 @@ describe('when telling the panels apart', () => {
it('should not mistake a stack panel for a wrap panel', () => isWrapPanel(stack).should.be.false);
it('should not mistake a wrap panel for a stack panel', () => isStackPanel(wrap).should.be.false);
it('should not mistake a grid for a dock panel', () => isDockPanel(grid).should.be.false);
it('should not mistake a canvas for a grouping panel', () => isCanvas(plain).should.be.false);
it('should leave a canvas unclaimed by the arranging guards', () =>
[isGrid(canvas), isDockPanel(canvas), isStackPanel(canvas), isWrapPanel(canvas)].should.have.members([false, false, false, false]));

it('should leave a plain panel unclaimed by every guard', () =>
[isGrid(plain), isDockPanel(plain), isStackPanel(plain), isWrapPanel(plain)].should.have.members([false, false, false, false]));
[isCanvas(plain), isGrid(plain), isDockPanel(plain), isStackPanel(plain), isWrapPanel(plain)].should.have.members([false, false, false, false, false]));
});
6 changes: 5 additions & 1 deletion Source/JavaScript/engine/panelKind.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

import { DockPanel, Grid, Panel, StackPanel, WrapPanel } from '@cratis/scene.model';
import { Canvas, DockPanel, Grid, Panel, StackPanel, WrapPanel } from '@cratis/scene.model';

/**
* Type guards telling the concrete panels apart, by the property each one alone declares - the same way
Expand All @@ -13,6 +13,10 @@ import { DockPanel, Grid, Panel, StackPanel, WrapPanel } from '@cratis/scene.mod
* whatever way its platform considers neutral.
*/

export function isCanvas(panel: Panel): panel is Canvas {
return 'extent' in panel;
}

export function isGrid(panel: Panel): panel is Grid {
return 'rows' in panel && 'columns' in panel;
}
Expand Down
21 changes: 21 additions & 0 deletions Source/JavaScript/model/elements/panels/Canvas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

import { Panel } from '../Panel';
import { Size } from '../../common';

/**
* Places its children at absolute coordinates within a coordinate space of its own. A child states where
* it sits through the `Canvas.Left`, `Canvas.Top`, `Canvas.Right` and `Canvas.Bottom` keys of its
* `properties`, which is how an attached property is carried in a model that has no attached properties
* of its own.
*
* Named for `@cratis/components`' `Canvas`, which is the surface a Cratis application actually draws one
* on. This is a panel in the element tree, and is not the same concept as a `FreeformArrangement` - that
* arranges a *layout slot's* content with one placement variant per size class.
*/
export interface Canvas extends Panel {
extent: Size;
}

export const CanvasPropertyNames: (keyof Canvas)[] = ['extent'];
1 change: 1 addition & 0 deletions Source/JavaScript/model/elements/panels/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

export * from './Canvas';
export * from './StackPanel';
export * from './DockPanel';
export * from './WrapPanel';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

import { ColumnDefinition, Dock, DockPanel, Grid, GridUnitType, HorizontalAlignment, Orientation, RowDefinition, SceneElement, StackPanel, VerticalAlignment, Visibility, WrapPanel } from '@cratis/scene.model';
import { Canvas, ColumnDefinition, Dock, DockPanel, Grid, GridUnitType, HorizontalAlignment, Orientation, RowDefinition, SceneElement, StackPanel, VerticalAlignment, Visibility, WrapPanel } from '@cratis/scene.model';
import { childStyle, panelStyle } from '../renderer/panelLayout';

const base = {
Expand All @@ -28,6 +28,29 @@ const row = (over: Partial<RowDefinition> = {}): RowDefinition => ({ height: sta
const column = (over: Partial<ColumnDefinition> = {}): ColumnDefinition => ({ width: star(1), minimumWidth: 0, maximumWidth: Number.POSITIVE_INFINITY, ...over });

describe('when arranging a panel', () => {
it('should make a canvas the coordinate space its children are placed against', () => {
const panel: Canvas = { ...base, extent: {} };
panelStyle(panel)!.position!.should.equal('relative');
});

it('should size a canvas to its extent', () => {
const panel: Canvas = { ...base, extent: { width: 1280, height: 800 } };
const style = panelStyle(panel)!;
[style.width, style.height].should.have.members([1280, 800]);
});

it('should place a canvas child at the edges it names', () => {
const panel: Canvas = { ...base, extent: {} };
const style = childStyle(panel, child({ 'Canvas.Left': 40, 'Canvas.Top': 24 }), 0)!;
style.position!.should.equal('absolute');
[style.left, style.top].should.have.members([40, 24]);
});

it('should leave a canvas child that names no edge in flow', () => {
const panel: Canvas = { ...base, extent: {} };
(childStyle(panel, child(), 0) === undefined).should.be.true;
});

it('should lay a stack panel out along its orientation', () => {
const panel: StackPanel = { ...base, orientation: Orientation.Horizontal, spacing: 8 };
panelStyle(panel)!.flexDirection!.should.equal('row');
Expand Down
23 changes: 22 additions & 1 deletion Source/JavaScript/react/renderer/panelLayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

import type { CSSProperties } from 'react';
import { isDockPanel, isGrid, isStackPanel, isWrapPanel } from '@cratis/scene.engine';
import { isCanvas, isDockPanel, isGrid, isStackPanel, isWrapPanel } from '@cratis/scene.engine';
import { Dock, GridUnitType, Orientation } from '@cratis/scene.model';
import type { ColumnDefinition, GridLength, Panel, RowDefinition, SceneElement } from '@cratis/scene.model';

Expand Down Expand Up @@ -39,6 +39,16 @@ const number = (value: unknown): number | undefined => (typeof value === 'number

/** The CSS that arranges a panel's own children. */
export function panelStyle(panel: Panel): CSSProperties | undefined {
if (isCanvas(panel)) {
// Absolutely placed children need a positioned ancestor to be placed against, and the canvas is
// that ancestor rather than whatever happens to be above it.
return {
position: 'relative',
width: panel.extent?.width ?? undefined,
height: panel.extent?.height ?? undefined,
};
}

if (isGrid(panel)) {
return {
display: 'grid',
Expand Down Expand Up @@ -77,6 +87,17 @@ export function panelStyle(panel: Panel): CSSProperties | undefined {
export function childStyle(panel: Panel, child: SceneElement, index: number): CSSProperties | undefined {
const properties = child.properties ?? {};

if (isCanvas(panel)) {
const left = number(properties['Canvas.Left']);
const top = number(properties['Canvas.Top']);
const right = number(properties['Canvas.Right']);
const bottom = number(properties['Canvas.Bottom']);
// A child that names no edge is not placed by the canvas at all, so it keeps whatever flow the
// renderer would otherwise have given it rather than being pinned to the origin.
if (left === undefined && top === undefined && right === undefined && bottom === undefined) return undefined;
return { position: 'absolute', left, top, right, bottom };
}

if (isGrid(panel)) {
const row = number(properties['Grid.Row']);
const column = number(properties['Grid.Column']);
Expand Down
3 changes: 3 additions & 0 deletions scene-model-shape.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@
"Panel": [
"children"
],
"Canvas": [
"extent"
],
"ItemsControl": [
"itemsSource",
"itemTemplate"
Expand Down
Loading