Allow changing Opacity & Flip directly in Instance Properties Panel (#6935)

* This allows playing around with instances directly on the canvas instead of relying on the actions to flip or change opacity and needing to start a preview
This commit is contained in:
Clément Pasteau
2024-09-13 13:39:03 +02:00
committed by GitHub
parent 0ca26a865e
commit dc45f3dae5
47 changed files with 969 additions and 134 deletions
+18 -5
View File
@@ -27,7 +27,11 @@ InitialInstance::InitialInstance()
rotationX(0),
rotationY(0),
zOrder(0),
opacity(255),
layer(""),
flippedX(false),
flippedY(false),
flippedZ(false),
customSize(false),
customDepth(false),
width(0),
@@ -57,7 +61,11 @@ void InitialInstance::UnserializeFrom(const SerializerElement& element) {
SetHasCustomDepth(false);
}
SetZOrder(element.GetIntAttribute("zOrder", 0, "plan"));
SetOpacity(element.GetIntAttribute("opacity", 255));
SetLayer(element.GetStringAttribute("layer"));
SetFlippedX(element.GetBoolAttribute("flippedX", false));
SetFlippedY(element.GetBoolAttribute("flippedY", false));
SetFlippedZ(element.GetBoolAttribute("flippedZ", false));
SetLocked(element.GetBoolAttribute("locked", false));
SetSealed(element.GetBoolAttribute("sealed", false));
SetShouldKeepRatio(element.GetBoolAttribute("keepRatio", false));
@@ -113,6 +121,10 @@ void InitialInstance::SerializeTo(SerializerElement& element) const {
element.SetAttribute("y", GetY());
if (GetZ() != 0) element.SetAttribute("z", GetZ());
element.SetAttribute("zOrder", GetZOrder());
if (GetOpacity() != 255) element.SetAttribute("opacity", GetOpacity());
if (IsFlippedX()) element.SetAttribute("flippedX", IsFlippedX());
if (IsFlippedY()) element.SetAttribute("flippedY", IsFlippedY());
if (IsFlippedZ()) element.SetAttribute("flippedZ", IsFlippedZ());
element.SetAttribute("layer", GetLayer());
element.SetAttribute("angle", GetAngle());
if (GetRotationX() != 0) element.SetAttribute("rotationX", GetRotationX());
@@ -155,8 +167,8 @@ InitialInstance& InitialInstance::ResetPersistentUuid() {
std::map<gd::String, gd::PropertyDescriptor>
InitialInstance::GetCustomProperties(
gd::ObjectsContainer &globalObjectsContainer,
gd::ObjectsContainer &objectsContainer) {
gd::ObjectsContainer& globalObjectsContainer,
gd::ObjectsContainer& objectsContainer) {
// Find an object
if (objectsContainer.HasObjectNamed(GetObjectName()))
return objectsContainer.GetObject(GetObjectName())
@@ -172,9 +184,10 @@ InitialInstance::GetCustomProperties(
}
bool InitialInstance::UpdateCustomProperty(
const gd::String &name, const gd::String &value,
gd::ObjectsContainer &globalObjectsContainer,
gd::ObjectsContainer &objectsContainer) {
const gd::String& name,
const gd::String& value,
gd::ObjectsContainer& globalObjectsContainer,
gd::ObjectsContainer& objectsContainer) {
if (objectsContainer.HasObjectNamed(GetObjectName()))
return objectsContainer.GetObject(GetObjectName())
.GetConfiguration()
+63 -19
View File
@@ -29,7 +29,7 @@ class GD_CORE_API InitialInstance {
* \brief Create an initial instance pointing to no object, at position (0,0).
*/
InitialInstance();
virtual ~InitialInstance(){};
virtual ~InitialInstance() {};
/**
* Must return a pointer to a copy of the object. A such method is needed to
@@ -123,6 +123,46 @@ class GD_CORE_API InitialInstance {
*/
void SetZOrder(int zOrder_) { zOrder = zOrder_; }
/**
* \brief Get Opacity.
*/
int GetOpacity() const { return opacity; }
/**
* \brief Set the opacity of the instance.
*/
void SetOpacity(int opacity_) { opacity = opacity_; }
/**
* \brief Return true if the instance is flipped on X axis.
*/
bool IsFlippedX() const { return flippedX; }
/**
* \brief Set whether the instance is flipped on X axis.
*/
void SetFlippedX(bool flippedX_) { flippedX = flippedX_; }
/**
* \brief Return true if the instance is flipped on Y axis.
*/
bool IsFlippedY() const { return flippedY; }
/**
* \brief Set whether the instance is flipped on Y axis.
*/
void SetFlippedY(bool flippedY_) { flippedY = flippedY_; }
/**
* \brief Return true if the instance is flipped on Z axis.
*/
bool IsFlippedZ() const { return flippedZ; }
/**
* \brief Set whether the instance is flipped on Z axis.
*/
void SetFlippedZ(bool flippedZ_) { flippedZ = flippedZ_; }
/**
* \brief Get the layer the instance belongs to.
*/
@@ -134,8 +174,9 @@ class GD_CORE_API InitialInstance {
void SetLayer(const gd::String& layer_) { layer = layer_; }
/**
* \brief Return true if the instance has a width/height which is different from its
* object default width/height. This is independent from `HasCustomDepth`.
* \brief Return true if the instance has a width/height which is different
* from its object default width/height. This is independent from
* `HasCustomDepth`.
*
* \see gd::Object
*/
@@ -150,15 +191,13 @@ class GD_CORE_API InitialInstance {
bool HasCustomDepth() const { return customDepth; }
/**
* \brief Set whether the instance has a width/height which is different from its
* object default width/height or not.
* This is independent from `SetHasCustomDepth`.
* \brief Set whether the instance has a width/height which is different from
* its object default width/height or not. This is independent from
* `SetHasCustomDepth`.
*
* \see gd::Object
*/
void SetHasCustomSize(bool hasCustomSize_) {
customSize = hasCustomSize_;
}
void SetHasCustomSize(bool hasCustomSize_) { customSize = hasCustomSize_; }
/**
* \brief Set whether the instance has a depth which is different from its
@@ -264,18 +303,19 @@ class GD_CORE_API InitialInstance {
* \note Common properties ( name, position... ) do not need to be
* inserted in this map
*/
std::map<gd::String, gd::PropertyDescriptor>
GetCustomProperties(gd::ObjectsContainer &globalObjectsContainer,
gd::ObjectsContainer &objectsContainer);
std::map<gd::String, gd::PropertyDescriptor> GetCustomProperties(
gd::ObjectsContainer& globalObjectsContainer,
gd::ObjectsContainer& objectsContainer);
/**
* \brief Update the property called \a name with the new \a value.
*
* \return false if the property could not be updated.
*/
bool UpdateCustomProperty(const gd::String &name, const gd::String &value,
gd::ObjectsContainer &globalObjectsContainer,
gd::ObjectsContainer &objectsContainer);
bool UpdateCustomProperty(const gd::String& name,
const gd::String& value,
gd::ObjectsContainer& globalObjectsContainer,
gd::ObjectsContainer& objectsContainer);
/**
* \brief Get the value of a double property stored in the instance.
@@ -343,6 +383,10 @@ class GD_CORE_API InitialInstance {
double rotationX; ///< Instance angle on X axis (for a 3D object)
double rotationY; ///< Instance angle on Y axis (for a 3D object)
int zOrder; ///< Instance Z order (for a 2D object)
int opacity; ///< Instance opacity
bool flippedX; ///< True if the instance is flipped on X axis
bool flippedY; ///< True if the instance is flipped on Y axis
bool flippedZ; ///< True if the instance is flipped on Z axis
gd::String layer; ///< Instance layer
bool customSize; ///< True if object has a custom width and height
bool customDepth; ///< True if object has a custom depth
@@ -352,13 +396,13 @@ class GD_CORE_API InitialInstance {
gd::VariablesContainer initialVariables; ///< Instance specific variables
bool locked; ///< True if the instance is locked
bool sealed; ///< True if the instance is sealed
bool keepRatio; ///< True if the instance's dimensions
/// should keep the same ratio.
bool keepRatio; ///< True if the instance's dimensions
/// should keep the same ratio.
mutable gd::String persistentUuid; ///< A persistent random version 4 UUID,
/// useful for hot reloading.
static gd::String*
badStringPropertyValue; ///< Empty string returned by GetRawStringProperty
static gd::String* badStringPropertyValue; ///< Empty string returned by
///< GetRawStringProperty
};
} // namespace gd
+11 -1
View File
@@ -164,8 +164,18 @@ namespace gdjs {
this.setWidth(initialInstanceData.width);
this.setHeight(initialInstanceData.height);
}
if (initialInstanceData.depth !== undefined)
if (initialInstanceData.depth !== undefined) {
this.setDepth(initialInstanceData.depth);
}
if (initialInstanceData.flippedX) {
this.flipX(initialInstanceData.flippedX);
}
if (initialInstanceData.flippedY) {
this.flipY(initialInstanceData.flippedY);
}
if (initialInstanceData.flippedZ) {
this.flipZ(initialInstanceData.flippedZ);
}
}
setX(x: float): void {
+11 -1
View File
@@ -72,8 +72,18 @@ namespace gdjs {
extraInitializationFromInitialInstance(initialInstanceData: InstanceData) {
super.extraInitializationFromInitialInstance(initialInstanceData);
if (initialInstanceData.depth !== undefined)
if (initialInstanceData.depth !== undefined) {
this.setDepth(initialInstanceData.depth);
}
if (initialInstanceData.flippedX) {
this.flipX(initialInstanceData.flippedX);
}
if (initialInstanceData.flippedY) {
this.flipY(initialInstanceData.flippedY);
}
if (initialInstanceData.flippedZ) {
this.flipZ(initialInstanceData.flippedZ);
}
}
/**
+27 -10
View File
@@ -2216,8 +2216,14 @@ module.exports = {
this._centerY / objectTextureFrame.height;
this._pixiTexturedObject.angle = this._instance.getAngle();
this._pixiTexturedObject.scale.x = width / objectTextureFrame.width;
this._pixiTexturedObject.scale.y = height / objectTextureFrame.height;
const scaleX =
(width / objectTextureFrame.width) *
(this._instance.isFlippedX() ? -1 : 1);
const scaleY =
(height / objectTextureFrame.height) *
(this._instance.isFlippedY() ? -1 : 1);
this._pixiTexturedObject.scale.x = scaleX;
this._pixiTexturedObject.scale.y = scaleY;
this._pixiTexturedObject.position.x =
this._instance.getX() +
@@ -2244,6 +2250,9 @@ module.exports = {
this._pixiFallbackObject.position.y =
this._instance.getY() + height / 2;
this._pixiFallbackObject.angle = this._instance.getAngle();
if (this._instance.isFlippedX()) this._pixiFallbackObject.scale.x = -1;
if (this._instance.isFlippedY()) this._pixiFallbackObject.scale.y = -1;
}
update() {
@@ -2393,12 +2402,16 @@ module.exports = {
RenderedInstance.toRad(this._instance.getAngle())
);
const scaleX = width * (this._instance.isFlippedX() ? -1 : 1);
const scaleY = height * (this._instance.isFlippedY() ? -1 : 1);
const scaleZ = depth * (this._instance.isFlippedZ() ? -1 : 1);
if (
width !== this._threeObject.scale.width ||
height !== this._threeObject.scale.height ||
depth !== this._threeObject.scale.depth
scaleX !== this._threeObject.scale.width ||
scaleY !== this._threeObject.scale.height ||
scaleZ !== this._threeObject.scale.depth
) {
this._threeObject.scale.set(width, height, depth);
this._threeObject.scale.set(scaleX, scaleY, scaleZ);
this.updateTextureUvMapping();
}
}
@@ -3186,12 +3199,16 @@ module.exports = {
RenderedInstance.toRad(this._instance.getAngle())
);
const scaleX = width * (this._instance.isFlippedX() ? -1 : 1);
const scaleY = height * (this._instance.isFlippedY() ? -1 : 1);
const scaleZ = depth * (this._instance.isFlippedZ() ? -1 : 1);
if (
width !== this._threeObject.scale.width ||
height !== this._threeObject.scale.height ||
depth !== this._threeObject.scale.depth
scaleX !== this._threeObject.scale.width ||
scaleY !== this._threeObject.scale.height ||
scaleZ !== this._threeObject.scale.depth
) {
this._threeObject.scale.set(width, height, depth);
this._threeObject.scale.set(scaleX, scaleY, scaleZ);
}
}
+7 -10
View File
@@ -66,13 +66,6 @@ module.exports = {
.setLabel(_('Base color'))
.setGroup(_('Appearance'));
objectProperties
.getOrCreate('opacity')
.setValue(objectContent.opacity.toString())
.setType('number')
.setLabel(_('Opacity (0-255)'))
.setGroup(_('Appearance'));
objectProperties
.getOrCreate('fontSize')
.setValue(objectContent.fontSize.toString())
@@ -545,9 +538,6 @@ module.exports = {
this._pixiObject.text = rawText;
}
const opacity = +properties.get('opacity').getValue();
this._pixiObject.alpha = opacity / 255;
const color = properties.get('color').getValue();
this._pixiObject.textStyles.default.fill = objectsRenderingService.rgbOrHexToHexNumber(
color
@@ -607,6 +597,13 @@ module.exports = {
this._pixiObject.dirty = true;
}
}
// Do not hide completely an object so it can still be manipulated
const alphaForDisplay = Math.max(
this._instance.getOpacity() / 255,
0.5
);
this._pixiObject.alpha = alphaForDisplay;
}
/**
+3
View File
@@ -192,6 +192,9 @@ namespace gdjs {
250
);
}
if (initialInstanceData.opacity !== undefined) {
this.setOpacity(initialInstanceData.opacity);
}
}
onDestroyed(): void {
+7 -10
View File
@@ -59,13 +59,6 @@ module.exports = {
.setType('textarea')
.setLabel(_('Text'));
objectProperties
.getOrCreate('opacity')
.setValue(objectContent.opacity.toString())
.setType('number')
.setLabel(_('Opacity (0-255)'))
.setGroup(_('Appearance'));
objectProperties
.getOrCreate('align')
.setValue(objectContent.align)
@@ -673,9 +666,6 @@ module.exports = {
const rawText = properties.get('text').getValue();
this._pixiObject.text = rawText;
const opacity = +properties.get('opacity').getValue();
this._pixiObject.alpha = opacity / 255;
const align = properties.get('align').getValue();
this._pixiObject.align = align;
@@ -739,6 +729,13 @@ module.exports = {
this._pixiObject.rotation = RenderedInstance.toRad(
this._instance.getAngle()
);
// Do not hide completely an object so it can still be manipulated
const alphaForDisplay = Math.max(
this._instance.getOpacity() / 255,
0.5
);
this._pixiObject.alpha = alphaForDisplay;
}
onRemovedFromScene() {
@@ -203,6 +203,9 @@ namespace gdjs {
if (initialInstanceData.customSize) {
this.setWrappingWidth(initialInstanceData.width);
}
if (initialInstanceData.opacity !== undefined) {
this.setOpacity(initialInstanceData.opacity);
}
}
onDestroyed(): void {
@@ -172,6 +172,9 @@ namespace gdjs {
this.setWidth(initialInstanceData.width);
this.setHeight(initialInstanceData.height);
}
if (initialInstanceData.opacity !== undefined) {
this.setOpacity(initialInstanceData.opacity);
}
}
/**
@@ -166,6 +166,19 @@ namespace gdjs {
return true;
}
/**
* Initialize the extra parameters that could be set for an instance.
* @param initialInstanceData The extra parameters
*/
extraInitializationFromInitialInstance(initialInstanceData: InstanceData) {
if (initialInstanceData.flippedX) {
this.flipX(initialInstanceData.flippedX);
}
if (initialInstanceData.flippedY) {
this.flipY(initialInstanceData.flippedY);
}
}
stepBehaviorsPreEvents(instanceContainer: gdjs.RuntimeInstanceContainer) {
//We redefine stepBehaviorsPreEvents just to clear the graphics before running events.
if (this._clearBetweenFrames) {
+14
View File
@@ -182,6 +182,20 @@ module.exports = {
this._instance.getAngle()
);
// Do not hide completely an object so it can still be manipulated
const alphaForDisplay = Math.max(
this._instance.getOpacity() / 255,
0.5
);
this._pixiObject.alpha = alphaForDisplay;
// Scale is already handled below, so we just apply the flip here.
this._pixiObject.scale.x =
Math.abs(this._pixiObject.scale.x) *
(this._instance.isFlippedX() ? -1 : 1);
this._pixiObject.scale.y =
Math.abs(this._pixiObject.scale.y) *
(this._instance.isFlippedY() ? -1 : 1);
this.setAnimation(this._instance.getRawDoubleProperty('animation'));
const width = this.getWidth();
+9
View File
@@ -208,6 +208,15 @@ namespace gdjs {
this.setSize(initialInstanceData.width, initialInstanceData.height);
this.invalidateHitboxes();
}
if (initialInstanceData.opacity !== undefined) {
this.setOpacity(initialInstanceData.opacity);
}
if (initialInstanceData.flippedX) {
this.flipX(initialInstanceData.flippedX);
}
if (initialInstanceData.flippedY) {
this.flipY(initialInstanceData.flippedY);
}
}
getDrawableX(): number {
+7
View File
@@ -789,6 +789,13 @@ module.exports = {
);
this._pixiGraphics.drawRect(0, 0, width, height);
this._pixiGraphics.endFill();
// Do not hide completely an object so it can still be manipulated
const alphaForDisplay = Math.max(
this._instance.getOpacity() / 255,
0.5
);
this._pixiObject.alpha = alphaForDisplay;
}
getDefaultWidth() {
@@ -254,6 +254,9 @@ namespace gdjs {
this.setWidth(initialInstanceData.width);
this.setHeight(initialInstanceData.height);
}
if (initialInstanceData.opacity !== undefined) {
this.setOpacity(initialInstanceData.opacity);
}
}
onScenePaused(runtimeScene: gdjs.RuntimeScene): void {
@@ -328,6 +328,9 @@ namespace gdjs {
} else {
this.setWrapping(false);
}
if (initialInstanceData.opacity !== undefined) {
this.setOpacity(initialInstanceData.opacity);
}
}
/**
+141 -19
View File
@@ -1611,6 +1611,7 @@ module.exports = {
this.tileMapPixiObject = new Tilemap.CompositeTilemap();
this._pixiObject = this.tileMapPixiObject;
this._editableTileMap = null;
// Implement `containsPoint` so that we can set `interactive` to true and
// the Tilemap will properly emit events when hovered/clicked.
@@ -1686,35 +1687,27 @@ module.exports = {
* This is used to reload the Tilemap
*/
updateTileMap() {
const tilemapObjectProperties = this._associatedObjectConfiguration.getProperties();
// Get the tileset resource to use
const tilemapAtlasImage = this._associatedObjectConfiguration
.getProperties()
const tilemapAtlasImage = tilemapObjectProperties
.get('tilemapAtlasImage')
.getValue();
const tilemapJsonFile = this._associatedObjectConfiguration
.getProperties()
const tilemapJsonFile = tilemapObjectProperties
.get('tilemapJsonFile')
.getValue();
const tilesetJsonFile = this._associatedObjectConfiguration
.getProperties()
const tilesetJsonFile = tilemapObjectProperties
.get('tilesetJsonFile')
.getValue();
const layerIndex = parseInt(
this._associatedObjectConfiguration
.getProperties()
.get('layerIndex')
.getValue(),
tilemapObjectProperties.get('layerIndex').getValue(),
10
);
const levelIndex = parseInt(
this._associatedObjectConfiguration
.getProperties()
.get('levelIndex')
.getValue(),
tilemapObjectProperties.get('levelIndex').getValue(),
10
);
const displayMode = this._associatedObjectConfiguration
.getProperties()
const displayMode = tilemapObjectProperties
.get('displayMode')
.getValue();
@@ -1755,6 +1748,8 @@ module.exports = {
return;
}
this._editableTileMap = tileMap;
/** @type {TileMapHelper.TileTextureCache} */
manager.getOrLoadTextureCache(
this._loadTileMapWithCallback.bind(this),
@@ -1774,12 +1769,13 @@ module.exports = {
return;
}
this._onLoadingSuccess();
if (!this._editableTileMap) return;
this.width = tileMap.getWidth();
this.height = tileMap.getHeight();
this.width = this._editableTileMap.getWidth();
this.height = this._editableTileMap.getHeight();
TilemapHelper.PixiTileMapHelper.updatePixiTileMap(
this.tileMapPixiObject,
tileMap,
this._editableTileMap,
textureCache,
displayMode,
layerIndex
@@ -1800,6 +1796,85 @@ module.exports = {
}
}
/**
* This is called to update the PIXI object on the scene editor, without reloading the tilemap.
*/
updatePixiTileMap() {
const tilemapObjectProperties = this._associatedObjectConfiguration.getProperties();
// Get the tileset resource to use
const tilemapAtlasImage = tilemapObjectProperties
.get('tilemapAtlasImage')
.getValue();
const tilemapJsonFile = tilemapObjectProperties
.get('tilemapJsonFile')
.getValue();
const tilesetJsonFile = tilemapObjectProperties
.get('tilesetJsonFile')
.getValue();
const layerIndex = parseInt(
tilemapObjectProperties.get('layerIndex').getValue(),
10
);
const levelIndex = parseInt(
tilemapObjectProperties.get('levelIndex').getValue(),
10
);
const displayMode = tilemapObjectProperties
.get('displayMode')
.getValue();
const tilemapResource = this._project
.getResourcesManager()
.getResource(tilemapJsonFile);
let metadata = {};
try {
const tilemapMetadataAsString = tilemapResource.getMetadata();
if (tilemapMetadataAsString)
metadata = JSON.parse(tilemapMetadataAsString);
} catch (error) {
console.warn('Malformed metadata in a tilemap object:', error);
}
const mapping = metadata.embeddedResourcesMapping || {};
/** @type {TileMapHelper.TileMapManager} */
const manager = TilemapHelper.TileMapManager.getManager(this._project);
/** @type {TileMapHelper.TileTextureCache} */
manager.getOrLoadTextureCache(
this._loadTileMapWithCallback.bind(this),
(textureName) =>
this._pixiResourcesLoader.getPIXITexture(
this._project,
mapping[textureName] || textureName
),
tilemapAtlasImage,
tilemapJsonFile,
tilesetJsonFile,
levelIndex,
(textureCache) => {
if (!textureCache) {
this._onLoadingError();
// getOrLoadTextureCache already log warns and errors.
return;
}
this._onLoadingSuccess();
if (!this._editableTileMap) return;
this.width = this._editableTileMap.getWidth();
this.height = this._editableTileMap.getHeight();
TilemapHelper.PixiTileMapHelper.updatePixiTileMap(
this.tileMapPixiObject,
this._editableTileMap,
textureCache,
displayMode,
layerIndex
);
}
);
}
// GDJS doesn't use Promise to avoid allocation.
_loadTileMapWithCallback(tilemapJsonFile, tilesetJsonFile, callback) {
this._loadTileMap(tilemapJsonFile, tilesetJsonFile).then(callback);
@@ -1870,6 +1945,30 @@ module.exports = {
this._pixiObject.rotation = RenderedInstance.toRad(
this._instance.getAngle()
);
// Update the opacity, if needed.
// Do not hide completely an object so it can still be manipulated
const alphaForDisplay = Math.max(
this._instance.getOpacity() / 255,
0.5
);
if (
this._editableTileMap &&
this._pixiObject.alpha !== alphaForDisplay
) {
this._pixiObject.alpha = alphaForDisplay;
for (const layer of this._editableTileMap.getLayers()) {
// Only update layers that are of type TileMapHelper.EditableTileMapLayer.
// @ts-ignore - only this type of layer has setAlpha.
if (layer.setAlpha) {
const editableLayer = /** @type {TileMapHelper.EditableTileMapLayer} */ (layer);
editableLayer.setAlpha(alphaForDisplay);
}
}
// Only update the tilemap if the alpha has changed.
this.updatePixiTileMap();
}
}
/**
@@ -2148,6 +2247,9 @@ module.exports = {
}
}
/**
* This is called to update the PIXI object on the scene editor, without reloading the tilemap.
*/
updatePixiTileMap() {
const atlasImageResourceName = this._associatedObjectConfiguration
.getProperties()
@@ -2266,6 +2368,26 @@ module.exports = {
objectToChange.rotation = RenderedInstance.toRad(
this._instance.getAngle()
);
// Update the opacity, if needed.
// Do not hide completely an object so it can still be manipulated
const alphaForDisplay = Math.max(
this._instance.getOpacity() / 255,
0.5
);
if (this._editableTileMap && objectToChange.alpha !== alphaForDisplay) {
objectToChange.alpha = alphaForDisplay;
for (const layer of this._editableTileMap.getLayers()) {
// Only update layers that are of type TileMapHelper.EditableTileMapLayer.
// @ts-ignore - only this type of layer has setAlpha.
if (layer.setAlpha) {
const editableLayer = /** @type {TileMapHelper.EditableTileMapLayer} */ (layer);
editableLayer.setAlpha(alphaForDisplay);
}
}
this.updatePixiTileMap();
}
}
/**
@@ -186,11 +186,14 @@ namespace gdjs {
// 2. Update the renderer so that it updates the tilemap object
// (used for width and position calculations).
this._loadInitialTileMap((tileMap: TileMapHelper.EditableTileMap) => {
// 3. Set custom dimensions if applicable.
// 3. Set custom dimensions & opacity if applicable.
if (initialInstanceData.customSize) {
this.setWidth(initialInstanceData.width);
this.setHeight(initialInstanceData.height);
}
if (initialInstanceData.opacity !== undefined) {
this.setOpacity(initialInstanceData.opacity);
}
// 4. Update position (calculations based on renderer's dimensions).
this._renderer.updatePosition();
@@ -203,6 +203,9 @@ namespace gdjs {
this.setWidth(initialInstanceData.width);
this.setHeight(initialInstanceData.height);
}
if (initialInstanceData.opacity !== undefined) {
this.setOpacity(initialInstanceData.opacity);
}
}
private _updateTileMap(): void {
@@ -135,6 +135,9 @@ namespace gdjs {
this.setWidth(initialInstanceData.width);
this.setHeight(initialInstanceData.height);
}
if (initialInstanceData.opacity !== undefined) {
this.setOpacity(initialInstanceData.opacity);
}
}
/**
+7 -13
View File
@@ -62,12 +62,6 @@ module.exports = {
videoObject.getProperties = function (objectContent) {
var objectProperties = new gd.MapStringPropertyDescriptor();
objectProperties
.getOrCreate('Opacity')
.setValue(objectContent.opacity.toString())
.setType('number')
.setLabel(_('Video opacity (0-255)'))
.setGroup(_('Appearance'));
objectProperties
.getOrCreate('Looped')
.setValue(objectContent.loop ? 'true' : 'false')
@@ -625,13 +619,6 @@ module.exports = {
}
}
// Update opacity
const opacity = +this._associatedObjectConfiguration
.getProperties()
.get('Opacity')
.getValue();
this._pixiObject.alpha = opacity / 255;
// Read position and angle from the instance
this._pixiObject.position.x =
this._instance.getX() + this._pixiObject.width / 2;
@@ -645,6 +632,13 @@ module.exports = {
this._pixiObject.width = this.getCustomWidth();
this._pixiObject.height = this.getCustomHeight();
}
// Do not hide completely an object so it can still be manipulated
const alphaForDisplay = Math.max(
this._instance.getOpacity() / 255,
0.5
);
this._pixiObject.alpha = alphaForDisplay;
}
/**
+3
View File
@@ -155,6 +155,9 @@ namespace gdjs {
this.setWidth(initialInstanceData.width);
this.setHeight(initialInstanceData.height);
}
if (initialInstanceData.opacity !== undefined) {
this.setOpacity(initialInstanceData.opacity);
}
}
onDestroyed(): void {
+9
View File
@@ -166,6 +166,15 @@ namespace gdjs {
this.setWidth(initialInstanceData.width);
this.setHeight(initialInstanceData.height);
}
if (initialInstanceData.opacity !== undefined) {
this.setOpacity(initialInstanceData.opacity);
}
if (initialInstanceData.flippedX) {
this.flipX(initialInstanceData.flippedX);
}
if (initialInstanceData.flippedY) {
this.flipY(initialInstanceData.flippedY);
}
}
onDeletedFromScene(parent: gdjs.RuntimeInstanceContainer): void {
@@ -80,8 +80,10 @@ namespace gdjs {
this._object.getY() + this._pixiContainer.pivot.y * Math.abs(scaleY);
this._pixiContainer.rotation = gdjs.toRad(this._object.angle);
this._pixiContainer.scale.x = scaleX;
this._pixiContainer.scale.y = scaleY;
this._pixiContainer.scale.x =
scaleX * (this._object.isFlippedX() ? -1 : 1);
this._pixiContainer.scale.y =
scaleY * (this._object.isFlippedY() ? -1 : 1);
this._pixiContainer.visible = !this._object.hidden;
this._pixiContainer.alpha = opacity / 255;
+9
View File
@@ -184,6 +184,15 @@ namespace gdjs {
this.setWidth(initialInstanceData.width);
this.setHeight(initialInstanceData.height);
}
if (initialInstanceData.opacity !== undefined) {
this.setOpacity(initialInstanceData.opacity);
}
if (initialInstanceData.flippedX) {
this.flipX(initialInstanceData.flippedX);
}
if (initialInstanceData.flippedY) {
this.flipY(initialInstanceData.flippedY);
}
}
/**
+5
View File
@@ -254,6 +254,11 @@ declare interface InstanceData {
rotationY?: number;
zOrder: number;
opacity?: number;
flippedX?: boolean;
flippedY?: boolean;
flippedZ?: boolean;
customSize: boolean;
width: number;
+9
View File
@@ -1303,9 +1303,18 @@ interface InitialInstance {
void SetShouldKeepRatio(boolean keepRatio);
long GetZOrder();
void SetZOrder(long zOrder);
long GetOpacity();
void SetOpacity(long opacity);
[Const, Ref] DOMString GetLayer();
void SetLayer([Const] DOMString layer);
boolean IsFlippedX();
void SetFlippedX(boolean flippedX);
boolean IsFlippedY();
void SetFlippedY(boolean flippedY);
boolean IsFlippedZ();
void SetFlippedZ(boolean flippedZ);
void SetHasCustomSize(boolean enable);
boolean HasCustomSize();
void SetHasCustomDepth(boolean enable);
+8
View File
@@ -1088,8 +1088,16 @@ export class InitialInstance extends EmscriptenObject {
setShouldKeepRatio(keepRatio: boolean): void;
getZOrder(): number;
setZOrder(zOrder: number): void;
getOpacity(): number;
setOpacity(opacity: number): void;
getLayer(): string;
setLayer(layer: string): void;
isFlippedX(): boolean;
setFlippedX(flippedX: boolean): void;
isFlippedY(): boolean;
setFlippedY(flippedY: boolean): void;
isFlippedZ(): boolean;
setFlippedZ(flippedZ: boolean): void;
setHasCustomSize(enable: boolean): void;
hasCustomSize(): boolean;
setHasCustomDepth(enable: boolean): void;
+8
View File
@@ -23,8 +23,16 @@ declare class gdInitialInstance {
setShouldKeepRatio(keepRatio: boolean): void;
getZOrder(): number;
setZOrder(zOrder: number): void;
getOpacity(): number;
setOpacity(opacity: number): void;
getLayer(): string;
setLayer(layer: string): void;
isFlippedX(): boolean;
setFlippedX(flippedX: boolean): void;
isFlippedY(): boolean;
setFlippedY(flippedY: boolean): void;
isFlippedZ(): boolean;
setFlippedZ(flippedZ: boolean): void;
setHasCustomSize(enable: boolean): void;
hasCustomSize(): boolean;
setHasCustomDepth(enable: boolean): void;
@@ -27,6 +27,7 @@ import VerticallyCenterWithBar from '../UI/VerticallyCenterWithBar';
import GDevelopThemeContext from '../UI/Theme/GDevelopThemeContext';
import { textEllipsisStyle } from '../UI/TextEllipsis';
import CompactPropertiesEditorRowField from './CompactPropertiesEditorRowField';
import CompactToggleButtons from '../UI/CompactToggleButtons';
// An "instance" here is the objects for which properties are shown
export type Instance = Object; // This could be improved using generics.
@@ -42,6 +43,8 @@ export type ValueFieldCommonProperties = {|
disabled?: (instances: Array<gdInitialInstance>) => boolean,
onEditButtonBuildMenuTemplate?: (i18n: I18nType) => Array<MenuItemTemplate>,
onEditButtonClick?: () => void,
getValueFromDisplayedValue?: string => string,
getDisplayedValueFromValue?: string => string,
|};
// "Primitive" value fields are "simple" fields.
@@ -144,6 +147,18 @@ type ActionButton = {|
onClick: (instance: Instance) => void,
|};
type ToggleButtons = {|
name: string,
nonFieldType: 'toggleButtons',
buttons: Array<{|
name: string,
renderIcon: (className?: string) => React.Node,
tooltip: React.Node,
getValue: Instance => boolean,
setValue: (instance: Instance, newValue: boolean) => void,
|}>,
|};
// A value field is a primitive or a resource.
export type ValueField = PrimitiveValueField | ResourceField;
@@ -154,6 +169,7 @@ export type Field =
| SectionTitle
| Title
| ActionButton
| ToggleButtons
| VerticalCenterWithBar
| {|
name: string,
@@ -418,6 +434,8 @@ const CompactPropertiesEditor = ({
instances.forEach(i => onClickEndAdornment(i));
_onInstancesModified(instances);
},
getValueFromDisplayedValue: field.getValueFromDisplayedValue,
getDisplayedValueFromValue: field.getDisplayedValueFromValue,
};
if (field.renderLeftIcon || field.hideLabel) {
return (
@@ -676,6 +694,36 @@ const CompactPropertiesEditor = ({
[instances]
);
const renderToggleButtons = React.useCallback(
(field: ToggleButtons) => {
const buttons = field.buttons.map(button => {
// Button is toggled if all instances have a truthy value for it.
const isToggled =
instances.filter(instance => button.getValue(instance)).length ===
instances.length;
return {
id: button.name,
renderIcon: button.renderIcon,
tooltip: button.tooltip,
isActive: isToggled,
onClick: () => {
instances.forEach(instance =>
button.setValue(instance, !isToggled)
);
_onInstancesModified(instances);
},
};
});
return (
<React.Fragment key={`toggle-buttons-${field.name}`}>
<CompactToggleButtons id={field.name} buttons={buttons} />
</React.Fragment>
);
},
[instances, _onInstancesModified]
);
const renderResourceField = (field: ResourceField) => {
if (!project || !resourceManagementProps) {
console.error(
@@ -823,6 +871,8 @@ const CompactPropertiesEditor = ({
return renderSectionTitle(field);
} else if (field.nonFieldType === 'button') {
return renderButton(field);
} else if (field.nonFieldType === 'toggleButtons') {
return renderToggleButtons(field);
} else if (field.nonFieldType === 'verticalCenterWithBar') {
return renderVerticalCenterWithBar(field);
}
@@ -15,6 +15,9 @@ import LetterH from '../../UI/CustomSvgIcons/LetterH';
import LetterW from '../../UI/CustomSvgIcons/LetterW';
import LetterD from '../../UI/CustomSvgIcons/LetterD';
import LetterZ from '../../UI/CustomSvgIcons/LetterZ';
import Opacity from '../../UI/CustomSvgIcons/Opacity';
import FlipHorizontal from '../../UI/CustomSvgIcons/FlipHorizontal';
import FlipVertical from '../../UI/CustomSvgIcons/FlipVertical';
import Instance from '../../UI/CustomSvgIcons/Instance';
import Link from '../../UI/CustomSvgIcons/Link';
import Unlink from '../../UI/CustomSvgIcons/Unlink';
@@ -27,6 +30,7 @@ import Object2d from '../../UI/CustomSvgIcons/Object2d';
import RotateX from '../../UI/CustomSvgIcons/RotateX';
import RotateY from '../../UI/CustomSvgIcons/RotateY';
import RotateZ from '../../UI/CustomSvgIcons/RotateZ';
import FlipZ from '../../UI/CustomSvgIcons/FlipZ';
/**
* Applies ratio to value without intermediary value to avoid precision issues.
@@ -414,8 +418,73 @@ const getKeepRatioField = ({
getNextValue: (currentValue: boolean) => !currentValue,
});
const getOpacityField = ({ i18n }: {| i18n: I18nType |}) => ({
name: 'Opacity',
getLabel: () => i18n._(t`Opacity`),
valueType: 'number',
getValue: (instance: gdInitialInstance) => {
const opacity = instance.getOpacity();
return Math.round((opacity / 255) * 100);
},
setValue: (instance: gdInitialInstance, newValue: number) => {
const newOpacity = Math.round((newValue / 100) * 255);
const opacity = Math.max(0, Math.min(255, newOpacity));
instance.setOpacity(opacity);
},
renderLeftIcon: className => <Opacity className={className} />,
getDisplayedValueFromValue: (value: string): string => {
return `${value}%`;
},
getValueFromDisplayedValue: (displayedValue: string): string => {
return displayedValue.replace('%', '');
},
});
const getFlippableButtons = ({
i18n,
canFlipZ,
}: {|
i18n: I18nType,
canFlipZ: boolean,
|}) => ({
name: 'Flip',
nonFieldType: 'toggleButtons',
buttons: [
{
name: 'Flip horizontal',
renderIcon: className => <FlipHorizontal className={className} />,
tooltip: i18n._(t`Flip horizontally`),
getValue: (instance: gdInitialInstance): boolean => instance.isFlippedX(),
setValue: (instance: gdInitialInstance, newValue: boolean) =>
instance.setFlippedX(newValue),
},
{
name: 'Flip vertical',
tooltip: i18n._(t`Flip vertically`),
renderIcon: className => <FlipVertical className={className} />,
getValue: (instance: gdInitialInstance): boolean => instance.isFlippedY(),
setValue: (instance: gdInitialInstance, newValue: boolean) =>
instance.setFlippedY(newValue),
},
canFlipZ
? {
name: 'Flip Z',
tooltip: i18n._(t` Flip along Z axis`),
renderIcon: className => <FlipZ className={className} />,
getValue: (instance: gdInitialInstance): boolean =>
instance.isFlippedZ(),
setValue: (instance: gdInitialInstance, newValue: boolean) =>
instance.setFlippedZ(newValue),
}
: null,
].filter(Boolean),
});
export const makeSchema = ({
is3DInstance,
hasOpacity,
canBeFlippedXY,
canBeFlippedZ,
i18n,
forceUpdate,
onEditObjectByName,
@@ -423,6 +492,9 @@ export const makeSchema = ({
layersContainer,
}: {|
is3DInstance: boolean,
hasOpacity: boolean,
canBeFlippedXY: boolean,
canBeFlippedZ: boolean,
i18n: I18nType,
forceUpdate: () => void,
onEditObjectByName: (name: string) => void,
@@ -500,6 +572,15 @@ export const makeSchema = ({
},
],
},
hasOpacity
? {
name: 'Opacity',
type: 'row',
preventWrap: true,
removeSpacers: true,
children: [getOpacityField({ i18n })],
}
: null,
getLayerField({ i18n, layersContainer }),
{
name: 'Rotation',
@@ -507,10 +588,21 @@ export const makeSchema = ({
title: i18n._(t`Rotation`),
preventWrap: true,
removeSpacers: true,
children: getRotationXAndRotationYFields({ i18n }),
children: canBeFlippedXY
? [getFlippableButtons({ i18n, canFlipZ: canBeFlippedZ })]
: [],
},
getRotationZField({ i18n }),
];
{
name: 'Rotation X and Y',
type: 'row',
preventWrap: true,
removeSpacers: true,
children: [
...getRotationXAndRotationYFields({ i18n }),
getRotationZField({ i18n }),
],
},
].filter(Boolean);
}
return [
@@ -562,6 +654,15 @@ export const makeSchema = ({
},
],
},
hasOpacity
? {
name: 'Opacity',
type: 'row',
preventWrap: true,
removeSpacers: true,
children: [getOpacityField({ i18n })],
}
: null,
getLayerField({ i18n, layersContainer }),
{
name: 'Rotation',
@@ -569,9 +670,18 @@ export const makeSchema = ({
title: i18n._(t`Rotation`),
preventWrap: true,
removeSpacers: true,
children: canBeFlippedXY
? [getFlippableButtons({ i18n, canFlipZ: canBeFlippedZ })]
: [],
},
{
name: 'Rotation Z',
type: 'row',
preventWrap: true,
removeSpacers: true,
children: [getRotationZField({ i18n })],
},
];
].filter(Boolean);
};
export const reorderInstanceSchemaForCustomProperties = (
@@ -90,32 +90,6 @@ const CompactInstancePropertiesEditor = ({
}: Props) => {
const forceUpdate = useForceUpdate();
const schemaFor2D: Schema = React.useMemo(
() =>
makeSchema({
i18n,
is3DInstance: false,
onGetInstanceSize,
onEditObjectByName,
layersContainer,
forceUpdate,
}),
[i18n, onGetInstanceSize, onEditObjectByName, layersContainer, forceUpdate]
);
const schemaFor3D: Schema = React.useMemo(
() =>
makeSchema({
i18n,
is3DInstance: true,
onGetInstanceSize,
onEditObjectByName,
layersContainer,
forceUpdate,
}),
[i18n, onGetInstanceSize, onEditObjectByName, layersContainer, forceUpdate]
);
const instance = instances[0];
/**
* TODO: multiple instances support for variables list. Expected behavior should be:
@@ -144,10 +118,20 @@ const CompactInstancePropertiesEditor = ({
);
if (!object) return { object: undefined, instanceSchema: undefined };
const is3DInstance = gd.MetadataProvider.getObjectMetadata(
const objectMetadata = gd.MetadataProvider.getObjectMetadata(
project.getCurrentPlatform(),
object.getType()
).isRenderedIn3D();
);
const is3DInstance = objectMetadata.isRenderedIn3D();
const hasOpacity = objectMetadata.hasDefaultBehavior(
'OpacityCapability::OpacityBehavior'
);
const canBeFlippedXY = objectMetadata.hasDefaultBehavior(
'FlippableCapability::FlippableBehavior'
);
const canBeFlippedZ = objectMetadata.hasDefaultBehavior(
'Scene3D::Base3DBehavior'
);
const instanceSchemaForCustomProperties = propertiesMapToSchema(
properties,
(instance: gdInitialInstance) =>
@@ -168,11 +152,20 @@ const CompactInstancePropertiesEditor = ({
instanceSchemaForCustomProperties,
i18n
);
const instanceSchema = makeSchema({
i18n,
is3DInstance,
hasOpacity,
canBeFlippedXY,
canBeFlippedZ,
onGetInstanceSize,
onEditObjectByName,
layersContainer,
forceUpdate,
}).concat(reorderedInstanceSchemaForCustomProperties);
return {
object,
instanceSchema: is3DInstance
? schemaFor3D.concat(reorderedInstanceSchemaForCustomProperties)
: schemaFor2D.concat(reorderedInstanceSchemaForCustomProperties),
instanceSchema,
};
},
[
@@ -181,8 +174,10 @@ const CompactInstancePropertiesEditor = ({
objectsContainer,
project,
i18n,
schemaFor3D,
schemaFor2D,
forceUpdate,
layersContainer,
onGetInstanceSize,
onEditObjectByName,
]
);
@@ -387,6 +387,7 @@ export class ChildInstance {
x: number;
y: number;
z: number;
opacity: number;
_hasCustomSize: boolean;
_hasCustomDepth: boolean;
_customWidth: number;
@@ -397,6 +398,7 @@ export class ChildInstance {
this.x = 0;
this.y = 0;
this.z = 0;
this.opacity = 255;
this._customWidth = 0;
this._customHeight = 0;
this._customDepth = 0;
@@ -464,6 +466,26 @@ export class ChildInstance {
setZOrder(zOrder: number) {}
getOpacity() {
return this.opacity;
}
setOpacity(opacity: number) {
this.opacity = opacity;
}
isFlippedX() {
return false;
}
setFlippedX(flippedX: boolean) {}
isFlippedY() {
return false;
}
setFlippedY(flippedY: boolean) {}
getLayer() {
return '';
}
@@ -524,6 +524,13 @@ export default class RenderedCustomObjectInstance extends Rendered3DInstance
this._pixiObject.scale.x = 1;
this._pixiObject.scale.y = 1;
}
// Do not hide completely an object so it can still be manipulated
const alphaForDisplay = Math.max(this._instance.getOpacity() / 255, 0.5);
this._pixiObject.alpha = alphaForDisplay;
if (this._instance.isFlippedX()) this._pixiObject.scale.x *= -1;
if (this._instance.isFlippedY()) this._pixiObject.scale.y *= -1;
}
getDefaultWidth() {
@@ -37,6 +37,10 @@ export default function makeRenderer(iconPath: string) {
this._pixiObject.position.x = this._instance.getX();
this._pixiObject.position.y = this._instance.getY();
this._pixiObject.angle = this._instance.getAngle();
// Do not hide completely an object so it can still be manipulated
const alphaForDisplay = Math.max(this._instance.getOpacity() / 255, 0.5);
this._pixiObject.alpha = alphaForDisplay;
}
static getThumbnail(
@@ -76,6 +76,10 @@ export default class RenderedPanelSpriteInstance extends RenderedInstance {
if (this._width !== oldWidth || this._height !== oldHeight) {
this.updateWidthHeight();
}
// Do not hide completely an object so it can still be manipulated
const alphaForDisplay = Math.max(this._instance.getOpacity() / 255, 0.5);
this._pixiObject.alpha = alphaForDisplay;
}
makeObjectsAndUpdateTextures() {
@@ -44,6 +44,10 @@ export default class RenderedParticleEmitterInstance extends RenderedInstance {
update() {
this._pixiObject.position.x = this._instance.getX();
this._pixiObject.position.y = this._instance.getY();
// Do not hide completely an object so it can still be manipulated
const alphaForDisplay = Math.max(this._instance.getOpacity() / 255, 0.5);
this._pixiObject.alpha = alphaForDisplay;
this.updateGraphics();
}
@@ -133,7 +133,11 @@ export default class RenderedSprite3DInstance extends Rendered3DInstance {
threeObject.position.y += this._instance.getY();
threeObject.position.z += this._instance.getZ();
threeObject.scale.set(width, height, 1);
const scaleX = width * (this._instance.isFlippedX() ? -1 : 1);
const scaleY = height * (this._instance.isFlippedY() ? -1 : 1);
const scaleZ = 1 * (this._instance.isFlippedZ() ? -1 : 1);
threeObject.scale.set(scaleX, scaleY, scaleZ);
}
updateSprite(): boolean {
@@ -118,6 +118,13 @@ export default class RenderedSpriteInstance extends RenderedInstance {
this._pixiObject.position.y =
this._instance.getY() +
(this._centerY - this._originY) * Math.abs(this._pixiObject.scale.y);
// Do not hide completely an object so it can still be manipulated
const alphaForDisplay = Math.max(this._instance.getOpacity() / 255, 0.5);
this._pixiObject.alpha = alphaForDisplay;
if (this._instance.isFlippedX()) this._pixiObject.scale.x *= -1;
if (this._instance.isFlippedY()) this._pixiObject.scale.y *= -1;
}
updateSprite(): boolean {
@@ -235,11 +242,15 @@ export default class RenderedSpriteInstance extends RenderedInstance {
getCenterX(): number {
if (!this._sprite || !this._pixiObject) return 0;
return this._centerX * this._pixiObject.scale.x; // This is equivalent to `this._animationFrame.center.x * Math.abs(this._scaleX)` in the runtime.
return (
this._centerX * Math.abs(this._pixiObject.scale.x) // This is equivalent to `this._animationFrame.center.x * Math.abs(this._scaleX)` in the runtime.
);
}
getCenterY(): number {
if (!this._sprite || !this._pixiObject) return 0;
return this._centerY * this._pixiObject.scale.y; // This is equivalent to `this._animationFrame.center.y * Math.abs(this._scaleY)` in the runtime.
return (
this._centerY * Math.abs(this._pixiObject.scale.y) // This is equivalent to `this._animationFrame.center.y * Math.abs(this._scaleY)` in the runtime.
);
}
}
@@ -209,6 +209,10 @@ export default class RenderedTextInstance extends RenderedInstance {
this._pixiObject.rotation = RenderedInstance.toRad(
this._instance.getAngle()
);
// Do not hide completely an object so it can still be manipulated
const alphaForDisplay = Math.max(this._instance.getOpacity() / 255, 0.5);
this._pixiObject.alpha = alphaForDisplay;
}
getDefaultWidth() {
@@ -89,6 +89,10 @@ export default class RenderedTiledSpriteInstance extends RenderedInstance {
this._pixiObject.rotation = RenderedInstance.toRad(
this._instance.getAngle()
);
// Do not hide completely an object so it can still be manipulated
const alphaForDisplay = Math.max(this._instance.getOpacity() / 255, 0.5);
this._pixiObject.alpha = alphaForDisplay;
}
getDefaultWidth() {
@@ -48,6 +48,8 @@ type Props = {|
useLeftIconAsNumberControl?: boolean,
renderEndAdornmentOnHover?: (className: string) => React.Node,
onClickEndAdornment?: () => void,
getValueFromDisplayedValue?: string => string,
getDisplayedValueFromValue?: string => string,
errorText?: React.Node,
|};
@@ -57,6 +59,8 @@ const CompactSemiControlledNumberField = ({
onChange,
errorText,
commitOnBlur,
getValueFromDisplayedValue,
getDisplayedValueFromValue,
...otherProps
}: Props) => {
const textFieldRef = React.useRef<?CompactTextFieldInterface>(null);
@@ -137,16 +141,23 @@ const CompactSemiControlledNumberField = ({
[commitOnBlur, onChange]
);
const stringValue = getDisplayedValueFromValue
? getDisplayedValueFromValue(value.toString())
: value.toString();
return (
<div className={classes.container}>
<CompactTextField
type="text"
ref={textFieldRef}
value={focused ? temporaryValue : value.toString()}
value={focused ? temporaryValue : stringValue}
onChange={onChangeValue}
onFocus={event => {
setFocused(true);
setTemporaryValue(value.toString());
const originalStringValue = getValueFromDisplayedValue
? getValueFromDisplayedValue(stringValue)
: stringValue;
setTemporaryValue(originalStringValue);
}}
onKeyDown={event => {
if (shouldValidate(event)) {
@@ -204,7 +215,10 @@ const CompactSemiControlledNumberField = ({
}
}}
onBlur={event => {
if (!cancelEditionRef.current) onChangeValue(temporaryValue, 'blur');
const newValue = getDisplayedValueFromValue
? getDisplayedValueFromValue(temporaryValue)
: temporaryValue;
if (!cancelEditionRef.current) onChangeValue(newValue, 'blur');
setFocused(false);
setTemporaryValue('');
cancelEditionRef.current = false;
@@ -0,0 +1,42 @@
.container {
display: flex;
align-items: center;
background-color: var(--theme-text-field-default-background-color);
flex: 1;
}
.compactToggleButton {
border-radius: 4px;
color: var(--theme-text-default-color);
background-color: var(--theme-text-field-default-background-color);
transition: box-shadow 0.1s;
position: relative;
display: flex;
align-items: center;
justify-content: center;
flex: 1;
min-width: 0px;
margin: 3px;
border: 0;
}
.separator {
background-color: var(--theme-text-field-disabled-color);
height: 15px;
width: 1px;
margin: 0 1px;
}
/* svg tag is needed to be first priority compared to Material UI Custom SVG icon classes*/
svg.icon {
font-size: 20px;
color: var(--theme-text-default-color);
transition: color 0.1s linear;
}
.compactToggleButton.active {
background-color: var(--theme-icon-button-selected-background-color);
}
.compactToggleButton.active svg.icon {
color: var(--theme-icon-button-selected-color);
}
@@ -0,0 +1,60 @@
// @flow
import * as React from 'react';
import Tooltip from '@material-ui/core/Tooltip';
import classNames from 'classnames';
import classes from './CompactToggleButtons.module.css';
import { tooltipEnterDelay } from '../Tooltip';
type CompactToggleButton = {|
id: string,
renderIcon: (className?: string) => React.Node,
tooltip: React.Node,
onClick: () => void,
isActive: boolean,
|};
export type CompactToggleButtonsProps = {|
id: string,
buttons: CompactToggleButton[],
|};
const CompactToggleButtons = ({ id, buttons }: CompactToggleButtonsProps) => {
return (
<div
id={id}
className={classNames({
[classes.container]: true,
})}
>
{buttons.map((button, index) => (
<React.Fragment key={button.id}>
<Tooltip
title={button.tooltip}
enterDelay={tooltipEnterDelay}
placement="top"
>
<button
className={classNames({
[classes.compactToggleButton]: true,
[classes.active]: button.isActive,
})}
onClick={button.onClick}
>
{button.renderIcon(classes.icon)}
</button>
</Tooltip>
{index < buttons.length - 1 && (
<div
key={`spacer-${index}`}
className={classNames({
[classes.separator]: true,
})}
/>
)}
</React.Fragment>
))}
</div>
);
};
export default CompactToggleButtons;
+38
View File
@@ -0,0 +1,38 @@
import React from 'react';
import SvgIcon from '@material-ui/core/SvgIcon';
export default React.memo(props => (
<SvgIcon
{...props}
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M2.32922 17.3355C2.14398 16.965 2.29415 16.5145 2.66463 16.3293L3.56463 15.8793C3.93511 15.694 4.38562 15.8442 4.57086 16.2147C4.7561 16.5851 4.60594 17.0356 4.23546 17.2209L3.33546 17.6709C2.96497 17.8561 2.51447 17.706 2.32922 17.3355ZM5.92922 15.5355C5.74398 15.165 5.89415 14.7145 6.26463 14.5292L8.06463 13.6292C8.43511 13.444 8.88562 13.5942 9.07086 13.9646C9.2561 14.3351 9.10594 14.7856 8.73545 14.9709L6.93545 15.8709C6.56497 16.0561 6.11447 15.906 5.92922 15.5355ZM10.4292 13.2854C10.244 12.915 10.3941 12.4645 10.7646 12.2792L12.5646 11.3792C12.9351 11.194 13.3856 11.3441 13.5709 11.7146C13.7561 12.0851 13.6059 12.5356 13.2355 12.7208L11.4355 13.6208C11.065 13.8061 10.6145 13.6559 10.4292 13.2854ZM14.9292 11.0354C14.744 10.6649 14.8941 10.2144 15.2646 10.0292L17.0646 9.12917C17.4351 8.94393 17.8856 9.0941 18.0709 9.46458C18.2561 9.83506 18.1059 10.2856 17.7355 10.4708L15.9355 11.3708C15.565 11.5561 15.1145 11.4059 14.9292 11.0354ZM19.4292 8.78539C19.244 8.41491 19.3941 7.9644 19.7646 7.77916L20.6646 7.32915C21.0351 7.14391 21.4856 7.29408 21.6709 7.66456C21.8561 8.03504 21.7059 8.48555 21.3355 8.67079L20.4355 9.1208C20.065 9.30604 19.6145 9.15587 19.4292 8.78539Z"
fill="currentColor"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M21.6709 17.3355C21.4856 17.7059 21.0351 17.8561 20.6646 17.6709L19.7646 17.2209C19.3942 17.0356 19.244 16.5851 19.4292 16.2146C19.6145 15.8442 20.065 15.694 20.4355 15.8792L21.3355 16.3292C21.7059 16.5145 21.8561 16.965 21.6709 17.3355ZM18.0709 15.5355C17.8856 15.9059 17.4351 16.0561 17.0646 15.8709L15.2646 14.9709C14.8942 14.7856 14.744 14.3351 14.9292 13.9646C15.1145 13.5942 15.565 13.444 15.9355 13.6292L17.7355 14.5292C18.1059 14.7145 18.2561 15.165 18.0709 15.5355ZM13.5709 13.2855C13.3856 13.6559 12.9351 13.8061 12.5646 13.6209L10.7646 12.7209C10.3942 12.5356 10.244 12.0851 10.4292 11.7146C10.6145 11.3441 11.065 11.194 11.4355 11.3792L13.2355 12.2792C13.606 12.4645 13.7561 12.915 13.5709 13.2855ZM9.07088 11.0355C8.88563 11.4059 8.43513 11.5561 8.06465 11.3709L6.26465 10.4709C5.89416 10.2856 5.74399 9.83511 5.92924 9.46463C6.11448 9.09414 6.56498 8.94397 6.93547 9.12922L8.73547 10.0292C9.10595 10.2145 9.25612 10.665 9.07088 11.0355ZM4.57088 8.78545C4.38564 9.15593 3.93513 9.3061 3.56465 9.12085L2.66465 8.67085C2.29416 8.48561 2.144 8.03511 2.32924 7.66462C2.51448 7.29414 2.96498 7.14397 3.33547 7.32921L4.23547 7.77921C4.60595 7.96446 4.75612 8.41496 4.57088 8.78545Z"
fill="currentColor"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M8.34808 1.50476C7.25003 1.28516 6.43937 2.50911 7.07001 3.43445L11.0289 9.24331C11.4795 9.90457 12.4268 9.97795 12.974 9.39399L16.9328 5.16868C17.6029 4.4535 17.2268 3.2805 16.2658 3.0883L8.34808 1.50476ZM8.65439 3.09573L15.531 4.47104L12.0927 8.14074L8.65439 3.09573Z"
fill="currentColor"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M15.3263 22.2809C16.4823 22.6277 17.4098 21.2971 16.6846 20.3324L12.9409 15.3533C12.534 14.8121 11.7702 14.6933 11.2181 15.0852L7.40035 17.796C6.5518 18.3984 6.76803 19.7134 7.76483 20.0125L15.3263 22.2809ZM15.0282 20.6254L8.686 18.7228L11.8882 16.4492L15.0282 20.6254Z"
fill="currentColor"
/>
</SvgIcon>
));
@@ -0,0 +1,41 @@
import React from 'react';
import SvgIcon from '@material-ui/core/SvgIcon';
export default React.memo(props => (
<SvgIcon
{...props}
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M4.50008 3.66699C4.03984 3.66699 3.66675 4.04009 3.66675 4.50033V11.5003C3.66675 11.9606 4.03984 12.3337 4.50008 12.3337H11.5001C11.9603 12.3337 12.3334 11.9606 12.3334 11.5003V4.50033C12.3334 4.04008 11.9603 3.66699 11.5001 3.66699H4.50008ZM2.66675 4.50033C2.66675 3.4878 3.48756 2.66699 4.50008 2.66699H11.5001C12.5126 2.66699 13.3334 3.48781 13.3334 4.50033V11.5003C13.3334 12.5129 12.5126 13.3337 11.5001 13.3337H4.50008C3.48756 13.3337 2.66675 12.5129 2.66675 11.5003V4.50033Z"
fill="currentColor"
/>
<path
opacity="0.8"
fillRule="evenodd"
clipRule="evenodd"
d="M10.8536 4.81344C11.0489 5.0087 11.0489 5.32528 10.8536 5.52055L5.5203 10.8539C5.32504 11.0491 5.00846 11.0491 4.81319 10.8539C4.61793 10.6586 4.61793 10.342 4.81319 10.1468L10.1465 4.81344C10.3418 4.61818 10.6584 4.61818 10.8536 4.81344Z"
fill="currentColor"
/>
<path
opacity="0.5"
fillRule="evenodd"
clipRule="evenodd"
d="M11.0202 7.64645C11.2155 7.84171 11.2155 8.15829 11.0202 8.35355L8.35355 11.0202C8.15829 11.2155 7.84171 11.2155 7.64645 11.0202C7.45118 10.825 7.45118 10.5084 7.64645 10.3131L10.3131 7.64645C10.5084 7.45118 10.825 7.45118 11.0202 7.64645Z"
fill="currentColor"
/>
<path
opacity="0.3"
fillRule="evenodd"
clipRule="evenodd"
d="M11.6869 9.64645C11.8821 9.84171 11.8821 10.1583 11.6869 10.3536L10.3536 11.6869C10.1583 11.8821 9.84171 11.8821 9.64645 11.6869C9.45118 11.4916 9.45118 11.175 9.64645 10.9798L10.9798 9.64645C11.175 9.45118 11.4916 9.45118 11.6869 9.64645Z"
fill="currentColor"
/>
</SvgIcon>
));
@@ -0,0 +1,97 @@
// @flow
import * as React from 'react';
import muiDecorator from '../../ThemeDecorator';
import paperDecorator from '../../PaperDecorator';
import CompactToggleButtons from '../../../UI/CompactToggleButtons';
import { ColumnStackLayout } from '../../../UI/Layout';
import Layers from '../../../UI/CustomSvgIcons/Layers';
export default {
title: 'UI Building Blocks/CompactToggleButtons',
component: CompactToggleButtons,
decorators: [paperDecorator, muiDecorator],
};
export const Default = () => {
const [value, setValue] = React.useState<boolean>(false);
const [value1, setValue1] = React.useState<boolean>(true);
const [value2, setValue2] = React.useState<boolean>(false);
const [value3, setValue3] = React.useState<boolean>(true);
const [value4, setValue4] = React.useState<boolean>(false);
const [value5, setValue5] = React.useState<boolean>(false);
return (
<ColumnStackLayout expand>
<CompactToggleButtons
id="one-item"
buttons={[
{
id: 'button1',
renderIcon: className => <Layers className={className} />,
tooltip: 'Layer',
onClick: () => {
setValue(!value);
},
isActive: value,
},
]}
/>
<CompactToggleButtons
id="two-items"
buttons={[
{
id: 'button1',
renderIcon: className => <Layers className={className} />,
tooltip: 'Layer',
onClick: () => {
setValue1(!value1);
},
isActive: value1,
},
{
id: 'button2',
renderIcon: className => <Layers className={className} />,
tooltip: 'Layer',
onClick: () => {
setValue2(!value2);
},
isActive: value2,
},
]}
/>
<CompactToggleButtons
id="three-items"
buttons={[
{
id: 'button1',
renderIcon: className => <Layers className={className} />,
tooltip: 'Layer',
onClick: () => {
setValue3(!value3);
},
isActive: value3,
},
{
id: 'button2',
renderIcon: className => <Layers className={className} />,
tooltip: 'Layer',
onClick: () => {
setValue4(!value4);
},
isActive: value4,
},
{
id: 'button3',
renderIcon: className => <Layers className={className} />,
tooltip: 'Layer',
onClick: () => {
setValue5(!value5);
},
isActive: value5,
},
]}
/>
</ColumnStackLayout>
);
};