import { PropertySection } from './section.cp.js';
import { TilingLayout, ConstraintLayout, Label, component, ViewModel, part, add, without } from 'lively.morphic';
import { Rectangle, rect, pt, Color } from 'lively.graphics';
import { EnumSelector, DarkThemeList } from '../shared.cp.js';
import { signal } from 'lively.bindings';
const FIXED_ICON = '\uea16';
/*
* Depending on the morph provides controls to configure the resizing behavior of a morph
* inside a TilingLayout or the constraints of a morph controlled by a ConstraintLayout
* or plain morph.
*/
export class ConstraintsManagerModel extends ViewModel {
static get properties () {
return {
targetMorph: {},
bindings: {
get () {
return [
{ target: 'constraints', signal: 'changed', handler: 'confirmConstraintPolicies' }
];
}
},
expose: {
get () {
return ['focusOn'];
}
}
};
}
focusOn (aMorph) {
this.targetMorph = aMorph;
this.update();
}
/*
* Ensure that the proportional layout is present in the owner of the focused morph.
* This is nessecary to reify the constraints.
*/
ensureLayout (x, y) {
// skip if owner is world morph
if (!this.targetMorph) return;
const owner = this.targetMorph.owner;
if (owner.isWorld) return;
owner.withMetaDo({ reconcileChanges: true }, () => {
if (!owner.layout && (x !== 'fixed' || y !== 'fixed')) {
owner.layout = new ConstraintLayout({
submorphSettings: owner.submorphs.map(m => [m.name, { x: 'fixed', y: 'fixed' }])
});
}
});
return owner.layout;
}
/**
* Refresh the UI to reflect the currently stored shadow value.
*/
update () {
const target = this.targetMorph;
const owner = target.owner;
this.view.visible = false;
if (owner && !owner.isWorld && !owner.isHand && (!owner.layout || owner.layout.name() === 'Constraint')) {
this.view.visible = true;
}
if (this.view.visible) this.refreshConstraints();
}
/**
* Refresh the UI of the constraint controls based on the configuration
* of the currently focused morph.
*/
refreshConstraints () {
const { constraints } = this.models;
const { layout } = this.targetMorph.owner;
if (!layout) {
// display fixed height and left
constraints.verticalConstraint = 'fixed';
constraints.horizontalConstraint = 'fixed';
return;
}
if (layout.name() === 'Constraint') {
const policy = layout.settingsFor(this.targetMorph);
constraints.verticalConstraint = policy.y;
constraints.horizontalConstraint = policy.x;
}
}
/**
* Update the current morph's resizing policy inside the ConstraintLayout
* that controls the morph.
*/
confirmConstraintPolicies () {
const { constraints } = this.models;
const { horizontalConstraint, verticalConstraint } = constraints;
const layout = this.ensureLayout(horizontalConstraint, verticalConstraint);
if (!layout) return;
layout.changeSettingsFor(this.targetMorph, {
x: horizontalConstraint,
y: verticalConstraint
});
this.targetMorph.withMetaDo({ reconcileChanges: true }, () => {
this.targetMorph.owner.layout = layout.getSpec().submorphSettings.length > 0 ? layout : undefined;
});
}
}
/**
* Control the position/resize constraints of a morph inside a morph without any layout
* or a morph with a ConstraintLayout.
* A constraint is a policy that dictates how the different sides of a morph relate to its
* owner morph frame.
* This controller automatically creates ConstraintLayouts as needed to reify the constraints.
*/
export class ConstraintsControlModel extends ViewModel {
static get properties () {
return {
verticalConstraint: { defaultValue: 'fixed' },
horizontalConstraint: { defaultValue: 'fixed' },
bindings: {
get () {
return [
{ target: 'horizontal alignment selector', signal: 'selection', handler: 'selectHorizontalConstraint' },
{ target: 'vertical alignment selector', signal: 'selection', handler: 'selectVerticalConstraint' },
{ target: 'constraints simulator', signal: 'onMouseDown', handler: 'onMarkerClicked' }
];
}
}
};
}
onRefresh (prop) {
this.clearAllMarkers();
const {
topMarker, bottomMarker, verticalMarker,
verticalAlignmentSelector
} = this.ui;
verticalAlignmentSelector.selection = this.verticalConstraint;
switch (this.verticalConstraint) {
case 'scale':
break; // this is not visualized
case 'move':
bottomMarker.master.setState('active');
break;
case 'fixed':
topMarker.master.setState('active');
break;
case 'resize':
topMarker.master.setState('active');
bottomMarker.master.setState('active');
break;
case 'center':
verticalMarker.master.setState('active');
break;
}
const {
leftMarker, rightMarker, horizontalMarker,
horizontalAlignmentSelector
} = this.ui;
horizontalAlignmentSelector.selection = this.horizontalConstraint;
switch (this.horizontalConstraint) {
case 'scale':
break; // this is not visualized
case 'move':
rightMarker.master.setState('active');
break;
case 'fixed':
leftMarker.master.setState('active');
break;
case 'resize':
leftMarker.master.setState('active');
rightMarker.master.setState('active');
break;
case 'center':
horizontalMarker.master.setState('active');
break;
}
}
clearAllMarkers () {
this.view.getAllNamed(/marker/).forEach(m => {
m.master.setState(null);
});
}
/**
* Configures the vertical constraint behavior of a morph. This applies when
* the morph is controlled by a ConstraintLayout.
* The following constraint behaviors are supported (By the ConstraintLayout):
* scale: this scales the morph along the vertial direction when the container resizes.
* move: this moves the morph along the vertical direction when the container resizes. (Also known as Bottom)
* fixed: this leaves the morph at a fixed vertical offset when the container resizes. (Also known as Top)
* resize: this resizes the morph by the same amount that the container resizes (Also known as Top and Botton)
* center: this moves the morph vertically via its relative center to the container as the container resizes.
*
* @params {("scale"|"move"|"fixed"|"resize"|"center")} behavior - The behavior for the vertical constraints of
* the morph controlled by the proportional layout.
*/
selectVerticalConstraint (behavior) {
if (behavior) this.verticalConstraint = behavior;
signal(this.view, 'changed');
}
/**
* Same as setlectVerticalConstraint() but for horizontal direction.
*
* @params {("scale"|"move"|"fixed"|"resize"|"center")} behavior - The behavior for the vertical constraints of
* the morph controlled by the proportional layout.
*/
selectHorizontalConstraint (behavior) {
if (behavior) this.horizontalConstraint = behavior;
signal(this.view, 'changed');
}
onMarkerClicked (evt) {
const markerName = evt.targetMorph.name;
if (markerName.endsWith('marker')) {
// send the signal accordingly
this.selectVerticalConstraint(({
'top marker': 'fixed',
'bottom marker': 'move',
'vertical marker': 'center'
})[markerName]);
this.selectHorizontalConstraint(({
'left marker': 'fixed',
'right marker': 'move',
'horizontal marker': 'center'
})[markerName]);
}
}
}
const Plain = component({ name: 'plain', extent: pt(10, 14), fill: Color.transparent, nativeCursor: 'pointer' });
const Hovered = component({ name: 'marked', extent: pt(10, 14), fill: Color.rgba(128, 216, 255, 0.5), nativeCursor: 'pointer' });
const ConstraintMarker = component({
name: 'constraint marker',
layout: new TilingLayout({
align: 'center',
axisAlign: 'center'
}),
master: {
auto: Plain,
hover: Hovered
},
nativeCursor: 'pointer',
extent: pt(10, 14),
fill: Color.transparent,
submorphs: [{
name: 'accent',
reactsToPointer: false,
position: pt(4, 2),
extent: pt(2, 10),
borderColor: Color.rgb(178, 235, 242),
fill: Color.rgb(102, 102, 102)
}]
});
const ConstraintMarkerActive = component(ConstraintMarker, {
name: 'constraint marker active',
submorphs: [{
name: 'accent',
position: pt(2.5, 2),
fill: Color.rgb(178, 235, 242),
borderWidth: 1.5
}]
});
// fixme: Think about parametrizing components via component definition/policy
const ConstraintsSimulator = component({
name: 'constraints simulator',
borderColor: Color.darkGray,
borderWidth: 1,
extent: pt(75, 75),
fill: Color.rgba(0, 0, 0, 0),
position: pt(1.3, 12.1),
submorphs: [
part(ConstraintMarker, {
master: { states: { active: ConstraintMarkerActive } },
name: 'top marker',
tooltip: 'Resize with Top Border',
position: pt(32.9, 4.4)
}), part(ConstraintMarker, {
master: { states: { active: ConstraintMarkerActive } },
name: 'right marker',
tooltip: 'Resize with Right Border',
rotation: Math.PI / 2,
position: pt(71.6, 33.4)
}), part(ConstraintMarker, {
master: { states: { active: ConstraintMarkerActive } },
name: 'bottom marker',
tooltip: 'Resize with Bottom Border',
position: pt(32.7, 56.5)
}), part(ConstraintMarker, {
master: { states: { active: ConstraintMarkerActive } },
name: 'left marker',
rotation: Math.PI / 2,
position: pt(17, 32.8)
}), {
name: 'inner constraints',
borderWidth: 1,
borderColor: Color.rgb(102, 102, 102),
extent: pt(35, 35),
fill: Color.transparent,
position: pt(20, 20),
submorphs: [part(ConstraintMarker, {
master: { states: { active: ConstraintMarkerActive } },
name: 'vertical marker',
tooltip: 'Proportionally Fix Center Vertically',
height: 19,
position: pt(12.3, 8.1),
submorphs: [{ name: 'accent', height: 15 }]
}), part(ConstraintMarker, {
master: { states: { active: ConstraintMarkerActive } },
name: 'horizontal marker',
tooltip: 'Proportionally Fix Center Horizontally',
rotation: -1.5707963267948966,
height: 19,
position: pt(7.9, 22.7),
submorphs: [{ name: 'accent', height: 15 }]
})]
}]
});
const ConstraintsControl = component({
defaultViewModel: ConstraintsControlModel,
name: 'constraint controller',
layout: new TilingLayout({
align: 'center',
axis: 'column',
orderByIndex: true,
wrapSubmorphs: true,
padding: Rectangle.inset(20, 0, 0, 10),
spacing: 10
}),
borderColor: Color.rgb(23, 160, 251),
borderWidth: 0,
extent: pt(229, 84.9),
fill: Color.rgba(0, 0, 0, 0),
submorphs: [
part(ConstraintsSimulator, { name: 'constraints simulator' }),
part(EnumSelector, {
name: 'horizontal alignment selector',
tooltip: 'Choose Horizontal Alignment',
viewModel: {
listMaster: DarkThemeList,
listAlign: 'selection',
openListInWorld: true,
listHeight: 500,
items: [
{ string: 'Left and Right', value: 'resize', isListItem: true },
{ string: 'Center', value: 'center', isListItem: true },
{ string: 'Left', value: 'fixed', isListItem: true },
{ string: 'Right', value: 'move', isListItem: true },
{ string: 'Scale', value: 'scale', isListItem: true }
]
},
clipMode: 'hidden', // fixme: avoids weird css layout isse
extent: pt(128.8, 23.3),
submorphs: [add({
type: Label,
name: 'interactive label',
padding: rect(0, 0, 0, 0),
rotation: -1.5707963267948966,
fontColor: Color.white,
textAndAttributes: [FIXED_ICON, { fontFamily: 'Material Icons', fontSize: 18 }]
}, 'label'), {
name: 'label',
fontSize: 12
}]
}),
part(EnumSelector, {
name: 'vertical alignment selector',
tooltip: 'Choose Vertical Alignment',
viewModel: {
listMaster: DarkThemeList,
listAlign: 'selection',
openListInWorld: true,
listHeight: 500,
items: [
{ string: 'Top and Bottom', value: 'resize', isListItem: true },
{ string: 'Center', value: 'center', isListItem: true },
{ string: 'Top', value: 'fixed', isListItem: true },
{ string: 'Bottom', value: 'move', isListItem: true },
{ string: 'Scale', value: 'scale', isListItem: true }
]
},
extent: pt(128.5, 23.3),
clipMode: 'hidden', // fixme: avoids weird css layout isse
submorphs: [add({
type: Label,
name: 'interactive label',
padding: rect(0, 0, 0, 0),
fontColor: Color.white,
textAndAttributes: [FIXED_ICON, { fontFamily: 'Material Icons', fontSize: 18 }]
}, 'label'), {
name: 'label',
fontSize: 12
}]
})]
});
const ConstraintSizeSelectorDefault = component({
name: 'constraint size selector default',
extent: pt(38.6, 10),
clipMode: 'hidden',
nativeCursor: 'pointer',
fill: Color.transparent,
layout: new TilingLayout({ axis: 'row', align: 'center', axisAlign: 'center' }),
submorphs: [{
type: Label,
name: 'caret',
reactsToPointer: false,
fontColor: Color.rgb(176, 190, 197),
padding: rect(3, 0, 0, 0),
extent: pt(17, 17),
position: pt(9.8, -2),
borderRadius: 2,
fontFamily: 'Material Icons',
textAndAttributes: [
'\ue5ce', { fontSize: 14 }
]
}]
});
const ConstraintsManager = component(PropertySection, {
defaultViewModel: ConstraintsManagerModel,
name: 'constraints control',
extent: pt(250.6, 145),
submorphs: [{
name: 'h floater',
submorphs: [{
name: 'section headline',
textAndAttributes: ['Constraints', null]
}, without('add button')]
},
add(part(ConstraintsControl, { name: 'constraints' }))
]
});
export { ConstraintsManager, ConstraintsControl, ConstraintsSimulator, ConstraintSizeSelectorDefault, ConstraintMarker, ConstraintMarkerActive };