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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ Generate the build system files with CMake.
For a standard desktop build with tests and examples enabled, run:

```bash
cmake . -B build -DYUP_ENABLE_TESTS=ON -DYUP_ENABLE_EXAMPLES=ON
cmake . -B build -DYUP_BUILD_TESTS=ON -DYUP_BUILD_EXAMPLES=ON
cmake --build build --config Release --parallel 4
```

Expand All @@ -186,7 +186,7 @@ cmake --build build --config Release --parallel 4
Android will rely on cmake for configuration and gradlew will again call into cmake to build the native part of yup:

```bash
cmake -G "Ninja Multi-Config" . -B build -DYUP_TARGET_ANDROID=ON -DYUP_ENABLE_TESTS=ON -DYUP_ENABLE_EXAMPLES=ON
cmake -G "Ninja Multi-Config" . -B build -DYUP_TARGET_ANDROID=ON -DYUP_BUILD_TESTS=ON -DYUP_BUILD_EXAMPLES=ON
cd build/examples/render
./gradlew assembleRelease
# ./gradlew assembleDebug
Expand Down
10 changes: 5 additions & 5 deletions cmake/yup_standalone.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ function (yup_standalone_app)

# ==== Enable profiling
if (YUP_ENABLE_PROFILING AND NOT "${target_name}" STREQUAL "yup_tests")
list (APPEND additional_definitions YUP_ENABLE_PROFILING=1 YUP_ENABLE_PROFILING=1)
list (APPEND additional_definitions YUP_ENABLE_PROFILING=1)
list (APPEND additional_libraries perfetto::perfetto)
endif()

Expand Down Expand Up @@ -184,11 +184,11 @@ function (yup_standalone_app)
-sFETCH=1
#-sASYNCIFY=1
-sEXPORTED_RUNTIME_METHODS=ccall,cwrap
-sDEFAULT_LIBRARY_FUNCS_TO_INCLUDE='$dynCall','$stackTrace'
--shell-file "${YUP_ARG_CUSTOM_SHELL}")
-sDEFAULT_LIBRARY_FUNCS_TO_INCLUDE='$dynCall'
--shell-file=${YUP_ARG_CUSTOM_SHELL})

foreach (preload_file ${YUP_ARG_PRELOAD_FILES})
list (APPEND additional_link_options --preload-file ${preload_file})
foreach (preload_file IN ITEMS ${YUP_ARG_PRELOAD_FILES})
list (APPEND additional_link_options "--preload-file=${preload_file}")
endforeach()

set (target_copy_dest "$<TARGET_FILE_DIR:${target_name}>")
Expand Down
4 changes: 2 additions & 2 deletions modules/yup_core/system/yup_SystemStats.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -252,8 +252,8 @@ String SystemStats::getStackBacktrace()

#elif YUP_EMSCRIPTEN
std::string temporaryStack;
temporaryStack.resize (10 * EM_ASM_INT_V ({ return (lengthBytesUTF8 || Module.lengthBytesUTF8) (stackTrace()); }));
EM_ASM_ARGS ({ (stringToUTF8 || Module.stringToUTF8) (stackTrace(), $0, $1); }, temporaryStack.data(), temporaryStack.size());
temporaryStack.resize (emscripten_get_callstack (EM_LOG_C_STACK, nullptr, 0));
emscripten_get_callstack (EM_LOG_C_STACK, temporaryStack.data(), static_cast<int> (temporaryStack.size()));
result << temporaryStack.c_str();

#elif YUP_WINDOWS
Expand Down
169 changes: 155 additions & 14 deletions modules/yup_data_model/tree/yup_DataTree.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,103 @@ namespace yup

//==============================================================================

namespace
{
var coerceAttributeValue (const Identifier& nodeType,
const Identifier& propertyName,
const String& rawValue,
const ReferenceCountedObjectPtr<DataTreeSchema>& schema)
{
if (schema == nullptr)
return var (rawValue);

auto info = schema->getPropertyInfo (nodeType, propertyName);
if (info.type.isEmpty())
return var (rawValue);

const auto trimmed = rawValue.trim();

const auto looksLikeInteger = [] (const String& text)
{
if (text.isEmpty())
return false;

int start = 0;
if (text.startsWithChar ('-') || text.startsWithChar ('+'))
start = 1;

if (start == text.length())
return false;

for (int i = start; i < text.length(); ++i)
{
if (! CharacterFunctions::isDigit (text[i]))
return false;
}

return true;
};

const auto looksLikeNumber = [] (const String& text)
{
bool hasDigit = false;

for (int i = 0; i < text.length(); ++i)
{
const auto c = text[i];
if (CharacterFunctions::isDigit (c))
{
hasDigit = true;
continue;
}

if (c == '.' || c == '-' || c == '+' || c == 'e' || c == 'E')
continue;

return false;
}

return hasDigit;
};

if (info.type == "boolean")
{
if (trimmed.equalsIgnoreCase ("true") || trimmed == "1" || trimmed.equalsIgnoreCase ("yes"))
return var (true);

if (trimmed.equalsIgnoreCase ("false") || trimmed == "0" || trimmed.equalsIgnoreCase ("no"))
return var (false);

return var (rawValue);
}

if (info.type == "number" && looksLikeNumber (trimmed))
{
if (looksLikeInteger (trimmed))
return var (trimmed.getLargeIntValue());

return var (trimmed.getDoubleValue());
}

if ((info.type == "array" || info.type == "object") && trimmed.isNotEmpty())
{
var parsed;
if (JSON::parse (trimmed, parsed))
{
if (info.type == "array" && parsed.isArray())
return parsed;

if (info.type == "object" && parsed.isObject())
return parsed;
}
}

return var (rawValue);
}
} // namespace

//==============================================================================

class PropertySetAction : public UndoableAction
{
public:
Expand Down Expand Up @@ -911,13 +1008,7 @@ std::unique_ptr<XmlElement> DataTree::createXml() const
auto element = std::make_unique<XmlElement> (object->type.toString());

// Add properties as attributes
for (int i = 0; i < object->properties.size(); ++i)
{
const auto name = object->properties.getName (i);
const auto value = object->properties.getValueAt (i);

element->setAttribute (name.toString(), value.toString());
}
object->properties.copyToXmlAttributes (*element);

// Add children as child elements
for (const auto& child : object->children)
Expand All @@ -930,21 +1021,28 @@ std::unique_ptr<XmlElement> DataTree::createXml() const
}

DataTree DataTree::fromXml (const XmlElement& xml)
{
return fromXml (xml, nullptr);
}

DataTree DataTree::fromXml (const XmlElement& xml, ReferenceCountedObjectPtr<DataTreeSchema> schema)
{
DataTree tree (xml.getTagName());
const auto nodeType = tree.getType();

// Load properties from attributes
for (int i = 0; i < xml.getNumAttributes(); ++i)
{
const auto name = xml.getAttributeName (i);
const auto value = xml.getAttributeValue (i);
tree.setProperty (name, value);
auto name = xml.getAttributeName (i);
auto value = xml.getAttributeValue (i);

tree.setProperty (name, coerceAttributeValue (nodeType, name, value, schema));
}

// Load children from child elements
for (const auto* childXml : xml.getChildIterator())
{
auto child = fromXml (*childXml);
auto child = fromXml (*childXml, schema);
tree.addChild (child);
}

Expand Down Expand Up @@ -1475,6 +1573,38 @@ void DataTree::Transaction::moveChild (int currentIndex, int newIndex)
childChanges.push_back (change);
}

int DataTree::Transaction::getEffectiveChildCount() const
{
if (dataTree.object == nullptr)
return 0;

int count = dataTree.getNumChildren();

for (const auto& change : childChanges)
{
switch (change.type)
{
case ChildChange::Add:
++count;
break;

case ChildChange::Remove:
if (count > 0)
--count;
break;

case ChildChange::RemoveAll:
count = 0;
break;

case ChildChange::Move:
break; // No change in count
}
}

return std::max (0, count);
}

//==============================================================================

DataTree::ValidatedTransaction::ValidatedTransaction (DataTree& tree, ReferenceCountedObjectPtr<DataTreeSchema> schema, UndoManager* undoManager)
Expand Down Expand Up @@ -1555,8 +1685,8 @@ Result DataTree::ValidatedTransaction::addChild (const DataTree& child, int inde
if (! child.isValid())
return Result::fail ("Cannot add invalid child");

// TODO: Get current child count from the transaction's target tree
auto validationResult = schema->validateChildAddition (nodeType, child.getType(), 0);
const int effectiveChildCount = transaction->getEffectiveChildCount();
auto validationResult = schema->validateChildAddition (nodeType, child.getType(), effectiveChildCount);
if (validationResult.failed())
{
hasValidationErrors = true;
Expand Down Expand Up @@ -1588,7 +1718,18 @@ Result DataTree::ValidatedTransaction::removeChild (const DataTree& child)
if (! transaction || ! transaction->isActive() || ! schema)
return Result::fail ("Transaction is not active");

// TODO: Check minimum child count constraints
if (! schema->hasNodeType (nodeType))
return Result::fail ("Unknown node type: " + nodeType.toString());

const auto constraints = schema->getChildConstraints (nodeType);
const int currentCount = transaction->getEffectiveChildCount();
const int resultingCount = std::max (0, currentCount - 1);

if (resultingCount < constraints.minCount)
{
hasValidationErrors = true;
return Result::fail ("Cannot remove child: would violate minimum child count (" + String (constraints.minCount) + ")");
}

transaction->removeChild (child);
return Result::ok();
Expand Down
20 changes: 20 additions & 0 deletions modules/yup_data_model/tree/yup_DataTree.h
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,21 @@ class YUP_API DataTree
*/
static DataTree fromXml (const XmlElement& xml);

/**
Recreates a DataTree from an XmlElement using a schema to recover types.

Behaves like fromXml(), but uses the provided schema to coerce property values
back to their declared types (e.g., numbers, booleans) during deserialization.
When no schema is provided, properties are imported as strings, matching JUCE
ValueTree behaviour.

@param xml The XmlElement to deserialize from
@param schema Optional schema describing node/property types for coercion
@return A new DataTree representing the XML content, or invalid DataTree on failure
*/
static DataTree fromXml (const XmlElement& xml,
ReferenceCountedObjectPtr<DataTreeSchema> schema);

/**
Writes this DataTree to a binary stream in a compact format.

Expand Down Expand Up @@ -955,6 +970,11 @@ class YUP_API DataTree
*/
void moveChild (int currentIndex, int newIndex);

/**
Returns the effective number of children taking pending operations into account.
*/
int getEffectiveChildCount() const;

private:
friend class TransactionAction;

Expand Down
Loading
Loading