Further refactoring of text-controller-impl
[platform/core/uifw/dali-toolkit.git] / dali-toolkit / internal / text / text-controller-text-updater.cpp
1 /*
2  * Copyright (c) 2020 Samsung Electronics Co., Ltd.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  *
16  */
17
18 // CLASS HEADER
19 #include <dali-toolkit/internal/text/text-controller-text-updater.h>
20
21 // EXTERNAL INCLUDES
22 #include <memory.h>
23 #include <dali/integration-api/debug.h>
24
25 // INTERNAL INCLUDES
26 #include <dali-toolkit/internal/text/character-set-conversion.h>
27 #include <dali-toolkit/internal/text/markup-processor.h>
28 #include <dali-toolkit/internal/text/text-controller-impl.h>
29 #include <dali-toolkit/internal/text/text-editable-control-interface.h>
30
31 namespace
32 {
33
34 #if defined(DEBUG_ENABLED)
35 Debug::Filter* gLogFilter = Debug::Filter::New(Debug::NoLogging, true, "LOG_TEXT_CONTROLS");
36 #endif
37
38 } // namespace
39
40 namespace Dali
41 {
42
43 namespace Toolkit
44 {
45
46 namespace Text
47 {
48
49 void Controller::TextUpdater::SetText(Controller& controller, const std::string& text)
50 {
51   DALI_LOG_INFO( gLogFilter, Debug::Verbose, "Controller::SetText\n" );
52
53   Controller::Impl& impl = *controller.mImpl;
54
55   // Reset keyboard as text changed
56   impl.ResetInputMethodContext();
57
58   // Remove the previously set text and style.
59   ResetText(controller);
60
61   // Remove the style.
62   controller.ClearStyleData();
63
64   CharacterIndex lastCursorIndex = 0u;
65
66   EventData*& eventData = impl.mEventData;
67
68   if( nullptr != eventData )
69   {
70     // If popup shown then hide it by switching to Editing state
71     if( ( EventData::SELECTING == eventData->mState )          ||
72         ( EventData::EDITING_WITH_POPUP == eventData->mState ) ||
73         ( EventData::EDITING_WITH_GRAB_HANDLE == eventData->mState ) ||
74         ( EventData::EDITING_WITH_PASTE_POPUP == eventData->mState ) )
75     {
76       impl.ChangeState( EventData::EDITING );
77     }
78   }
79
80   if( !text.empty() )
81   {
82     ModelPtr& model = impl.mModel;
83     LogicalModelPtr& logicalModel = model->mLogicalModel;
84     model->mVisualModel->SetTextColor( impl.mTextColor );
85
86     MarkupProcessData markupProcessData( logicalModel->mColorRuns,
87                                          logicalModel->mFontDescriptionRuns,
88                                          logicalModel->mEmbeddedItems );
89
90     Length textSize = 0u;
91     const uint8_t* utf8 = NULL;
92     if( impl.mMarkupProcessorEnabled )
93     {
94       ProcessMarkupString( text, markupProcessData );
95       textSize = markupProcessData.markupProcessedText.size();
96
97       // This is a bit horrible but std::string returns a (signed) char*
98       utf8 = reinterpret_cast<const uint8_t*>( markupProcessData.markupProcessedText.c_str() );
99     }
100     else
101     {
102       textSize = text.size();
103
104       // This is a bit horrible but std::string returns a (signed) char*
105       utf8 = reinterpret_cast<const uint8_t*>( text.c_str() );
106     }
107
108     //  Convert text into UTF-32
109     Vector<Character>& utf32Characters = logicalModel->mText;
110     utf32Characters.Resize( textSize );
111
112     // Transform a text array encoded in utf8 into an array encoded in utf32.
113     // It returns the actual number of characters.
114     Length characterCount = Utf8ToUtf32( utf8, textSize, utf32Characters.Begin() );
115     utf32Characters.Resize( characterCount );
116
117     DALI_ASSERT_DEBUG( textSize >= characterCount && "Invalid UTF32 conversion length" );
118     DALI_LOG_INFO( gLogFilter, Debug::Verbose, "Controller::SetText %p UTF8 size %d, UTF32 size %d\n", &controller, textSize, logicalModel->mText.Count() );
119
120     // The characters to be added.
121     impl.mTextUpdateInfo.mNumberOfCharactersToAdd = logicalModel->mText.Count();
122
123     // To reset the cursor position
124     lastCursorIndex = characterCount;
125
126     // Update the rest of the model during size negotiation
127     impl.QueueModifyEvent( ModifyEvent::TEXT_REPLACED );
128
129     // The natural size needs to be re-calculated.
130     impl.mRecalculateNaturalSize = true;
131
132     // The text direction needs to be updated.
133     impl.mUpdateTextDirection = true;
134
135     // Apply modifications to the model
136     impl.mOperationsPending = ALL_OPERATIONS;
137   }
138   else
139   {
140     controller.ShowPlaceholderText();
141   }
142
143   // Resets the cursor position.
144   controller.ResetCursorPosition( lastCursorIndex );
145
146   // Scrolls the text to make the cursor visible.
147   controller.ResetScrollPosition();
148
149   impl.RequestRelayout();
150
151   if( nullptr != eventData )
152   {
153     // Cancel previously queued events
154     eventData->mEventQueue.clear();
155   }
156
157   // Do this last since it provides callbacks into application code.
158   if( NULL != impl.mEditableControlInterface )
159   {
160     impl.mEditableControlInterface->TextChanged();
161   }
162 }
163
164 void Controller::TextUpdater::InsertText(Controller& controller, const std::string& text, Controller::InsertType type)
165 {
166   Controller::Impl& impl = *controller.mImpl;
167   EventData*& eventData = impl.mEventData;
168
169   DALI_ASSERT_DEBUG( nullptr != eventData && "Unexpected InsertText" )
170
171   if( NULL == eventData )
172   {
173     return;
174   }
175
176   bool removedPrevious = false;
177   bool removedSelected = false;
178   bool maxLengthReached = false;
179
180   DALI_LOG_INFO( gLogFilter, Debug::Verbose, "Controller::InsertText %p %s (%s) mPrimaryCursorPosition %d mPreEditFlag %d mPreEditStartPosition %d mPreEditLength %d\n",
181                  &controller, text.c_str(), (COMMIT == type ? "COMMIT" : "PRE_EDIT"),
182                  eventData->mPrimaryCursorPosition, eventData->mPreEditFlag, eventData->mPreEditStartPosition, eventData->mPreEditLength );
183
184   ModelPtr& model = impl.mModel;
185   LogicalModelPtr& logicalModel = model->mLogicalModel;
186
187   // TODO: At the moment the underline runs are only for pre-edit.
188   model->mVisualModel->mUnderlineRuns.Clear();
189
190   // Remove the previous InputMethodContext pre-edit.
191   if( eventData->mPreEditFlag && ( 0u != eventData->mPreEditLength ) )
192   {
193     removedPrevious = RemoveText( controller,
194                                   -static_cast<int>( eventData->mPrimaryCursorPosition - eventData->mPreEditStartPosition ),
195                                   eventData->mPreEditLength,
196                                   DONT_UPDATE_INPUT_STYLE );
197
198     eventData->mPrimaryCursorPosition = eventData->mPreEditStartPosition;
199     eventData->mPreEditLength = 0u;
200   }
201   else
202   {
203     // Remove the previous Selection.
204     removedSelected = RemoveSelectedText(controller);
205
206   }
207
208   Vector<Character> utf32Characters;
209   Length characterCount = 0u;
210
211   if( !text.empty() )
212   {
213     //  Convert text into UTF-32
214     utf32Characters.Resize( text.size() );
215
216     // This is a bit horrible but std::string returns a (signed) char*
217     const uint8_t* utf8 = reinterpret_cast<const uint8_t*>( text.c_str() );
218
219     // Transform a text array encoded in utf8 into an array encoded in utf32.
220     // It returns the actual number of characters.
221     characterCount = Utf8ToUtf32( utf8, text.size(), utf32Characters.Begin() );
222     utf32Characters.Resize( characterCount );
223
224     DALI_ASSERT_DEBUG( text.size() >= utf32Characters.Count() && "Invalid UTF32 conversion length" );
225     DALI_LOG_INFO( gLogFilter, Debug::Verbose, "UTF8 size %d, UTF32 size %d\n", text.size(), utf32Characters.Count() );
226   }
227
228   if( 0u != utf32Characters.Count() ) // Check if Utf8ToUtf32 conversion succeeded
229   {
230     // The placeholder text is no longer needed
231     if( impl.IsShowingPlaceholderText() )
232     {
233       ResetText(controller);
234     }
235
236     impl.ChangeState( EventData::EDITING );
237
238     // Handle the InputMethodContext (predicitive text) state changes
239     if( COMMIT == type )
240     {
241       // InputMethodContext is no longer handling key-events
242       impl.ClearPreEditFlag();
243     }
244     else // PRE_EDIT
245     {
246       if( !eventData->mPreEditFlag )
247       {
248         DALI_LOG_INFO( gLogFilter, Debug::Verbose, "Entered PreEdit state\n" );
249
250         // Record the start of the pre-edit text
251         eventData->mPreEditStartPosition = eventData->mPrimaryCursorPosition;
252       }
253
254       eventData->mPreEditLength = utf32Characters.Count();
255       eventData->mPreEditFlag = true;
256
257       DALI_LOG_INFO( gLogFilter, Debug::Verbose, "mPreEditStartPosition %d mPreEditLength %d\n", eventData->mPreEditStartPosition, eventData->mPreEditLength );
258     }
259
260     const Length numberOfCharactersInModel = logicalModel->mText.Count();
261
262     // Restrict new text to fit within Maximum characters setting.
263     Length maxSizeOfNewText = std::min( ( impl.mMaximumNumberOfCharacters - numberOfCharactersInModel ), characterCount );
264     maxLengthReached = ( characterCount > maxSizeOfNewText );
265
266     // The cursor position.
267     CharacterIndex& cursorIndex = eventData->mPrimaryCursorPosition;
268
269     // Update the text's style.
270
271     // Updates the text style runs by adding characters.
272     logicalModel->UpdateTextStyleRuns( cursorIndex, maxSizeOfNewText );
273
274     // Get the character index from the cursor index.
275     const CharacterIndex styleIndex = ( cursorIndex > 0u ) ? cursorIndex - 1u : 0u;
276
277     // Retrieve the text's style for the given index.
278     InputStyle style;
279     impl.RetrieveDefaultInputStyle( style );
280     logicalModel->RetrieveStyle( styleIndex, style );
281
282     InputStyle& inputStyle = eventData->mInputStyle;
283
284     // Whether to add a new text color run.
285     const bool addColorRun = ( style.textColor != inputStyle.textColor ) && !inputStyle.isDefaultColor;
286
287     // Whether to add a new font run.
288     const bool addFontNameRun = ( style.familyName != inputStyle.familyName ) && inputStyle.isFamilyDefined;
289     const bool addFontWeightRun = ( style.weight != inputStyle.weight ) && inputStyle.isWeightDefined;
290     const bool addFontWidthRun = ( style.width != inputStyle.width ) && inputStyle.isWidthDefined;
291     const bool addFontSlantRun = ( style.slant != inputStyle.slant ) && inputStyle.isSlantDefined;
292     const bool addFontSizeRun = ( style.size != inputStyle.size ) && inputStyle.isSizeDefined ;
293
294     // Add style runs.
295     if( addColorRun )
296     {
297       const VectorBase::SizeType numberOfRuns = logicalModel->mColorRuns.Count();
298       logicalModel->mColorRuns.Resize( numberOfRuns + 1u );
299
300       ColorRun& colorRun = *( logicalModel->mColorRuns.Begin() + numberOfRuns );
301       colorRun.color = inputStyle.textColor;
302       colorRun.characterRun.characterIndex = cursorIndex;
303       colorRun.characterRun.numberOfCharacters = maxSizeOfNewText;
304     }
305
306     if( addFontNameRun   ||
307         addFontWeightRun ||
308         addFontWidthRun  ||
309         addFontSlantRun  ||
310         addFontSizeRun )
311     {
312       const VectorBase::SizeType numberOfRuns = logicalModel->mFontDescriptionRuns.Count();
313       logicalModel->mFontDescriptionRuns.Resize( numberOfRuns + 1u );
314
315       FontDescriptionRun& fontDescriptionRun = *( logicalModel->mFontDescriptionRuns.Begin() + numberOfRuns );
316
317       if( addFontNameRun )
318       {
319         fontDescriptionRun.familyLength = inputStyle.familyName.size();
320         fontDescriptionRun.familyName = new char[fontDescriptionRun.familyLength];
321         memcpy( fontDescriptionRun.familyName, inputStyle.familyName.c_str(), fontDescriptionRun.familyLength );
322         fontDescriptionRun.familyDefined = true;
323
324         // The memory allocated for the font family name is freed when the font description is removed from the logical model.
325       }
326
327       if( addFontWeightRun )
328       {
329         fontDescriptionRun.weight = inputStyle.weight;
330         fontDescriptionRun.weightDefined = true;
331       }
332
333       if( addFontWidthRun )
334       {
335         fontDescriptionRun.width = inputStyle.width;
336         fontDescriptionRun.widthDefined = true;
337       }
338
339       if( addFontSlantRun )
340       {
341         fontDescriptionRun.slant = inputStyle.slant;
342         fontDescriptionRun.slantDefined = true;
343       }
344
345       if( addFontSizeRun )
346       {
347         fontDescriptionRun.size = static_cast<PointSize26Dot6>( inputStyle.size * impl.mFontSizeScale * 64.f );
348         fontDescriptionRun.sizeDefined = true;
349       }
350
351       fontDescriptionRun.characterRun.characterIndex = cursorIndex;
352       fontDescriptionRun.characterRun.numberOfCharacters = maxSizeOfNewText;
353     }
354
355     // Insert at current cursor position.
356     Vector<Character>& modifyText = logicalModel->mText;
357
358     auto pos = modifyText.End();
359     if( cursorIndex < numberOfCharactersInModel )
360     {
361       pos = modifyText.Begin() + cursorIndex;
362     }
363     unsigned int realPos = pos - modifyText.Begin();
364     modifyText.Insert( pos, utf32Characters.Begin(), utf32Characters.Begin() + maxSizeOfNewText );
365
366     if( NULL != impl.mEditableControlInterface )
367     {
368       impl.mEditableControlInterface->TextInserted( realPos, maxSizeOfNewText, text );
369     }
370
371     TextUpdateInfo& textUpdateInfo = impl.mTextUpdateInfo;
372
373     // Mark the first paragraph to be updated.
374     if( Layout::Engine::SINGLE_LINE_BOX == impl.mLayoutEngine.GetLayout() )
375     {
376       textUpdateInfo.mCharacterIndex = 0;
377       textUpdateInfo.mNumberOfCharactersToRemove = textUpdateInfo.mPreviousNumberOfCharacters;
378       textUpdateInfo.mNumberOfCharactersToAdd = numberOfCharactersInModel + maxSizeOfNewText;
379       textUpdateInfo.mClearAll = true;
380     }
381     else
382     {
383       textUpdateInfo.mCharacterIndex = std::min( cursorIndex, textUpdateInfo.mCharacterIndex );
384       textUpdateInfo.mNumberOfCharactersToAdd += maxSizeOfNewText;
385     }
386
387     // Update the cursor index.
388     cursorIndex += maxSizeOfNewText;
389
390     DALI_LOG_INFO( gLogFilter, Debug::Verbose, "Inserted %d characters, new size %d new cursor %d\n", maxSizeOfNewText, logicalModel->mText.Count(), eventData->mPrimaryCursorPosition );
391   }
392
393   if( ( 0u == logicalModel->mText.Count() ) &&
394       impl.IsPlaceholderAvailable() )
395   {
396     // Show place-holder if empty after removing the pre-edit text
397     controller.ShowPlaceholderText();
398     eventData->mUpdateCursorPosition = true;
399     impl.ClearPreEditFlag();
400   }
401   else if( removedPrevious ||
402            removedSelected ||
403            ( 0 != utf32Characters.Count() ) )
404   {
405     // Queue an inserted event
406     impl.QueueModifyEvent( ModifyEvent::TEXT_INSERTED );
407
408     eventData->mUpdateCursorPosition = true;
409     if( removedSelected )
410     {
411       eventData->mScrollAfterDelete = true;
412     }
413     else
414     {
415       eventData->mScrollAfterUpdatePosition = true;
416     }
417   }
418
419   if( maxLengthReached )
420   {
421     DALI_LOG_INFO( gLogFilter, Debug::Verbose, "MaxLengthReached (%d)\n", logicalModel->mText.Count() );
422
423     impl.ResetInputMethodContext();
424
425     if( NULL != impl.mEditableControlInterface )
426     {
427       // Do this last since it provides callbacks into application code
428       impl.mEditableControlInterface->MaxLengthReached();
429     }
430   }
431 }
432
433 void Controller::TextUpdater::PasteText(Controller& controller, const std::string& stringToPaste)
434 {
435   InsertText( controller, stringToPaste, Text::Controller::COMMIT );
436   Controller::Impl& impl = *controller.mImpl;
437   impl.ChangeState( EventData::EDITING );
438   impl.RequestRelayout();
439
440   if( NULL != impl.mEditableControlInterface )
441   {
442     // Do this last since it provides callbacks into application code
443     impl.mEditableControlInterface->TextChanged();
444   }
445 }
446
447 bool Controller::TextUpdater::RemoveText(
448     Controller& controller,
449     int cursorOffset,
450     int numberOfCharacters,
451     UpdateInputStyleType type )
452 {
453   bool removed = false;
454
455   Controller::Impl& impl = *controller.mImpl;
456   EventData*& eventData = impl.mEventData;
457
458   if( nullptr == eventData )
459   {
460     return removed;
461   }
462
463   ModelPtr& model = impl.mModel;
464   LogicalModelPtr& logicalModel = model->mLogicalModel;
465
466   DALI_LOG_INFO( gLogFilter, Debug::General, "Controller::RemoveText %p mText.Count() %d cursor %d cursorOffset %d numberOfCharacters %d\n",
467                  &controller, logicalModel->mText.Count(), eventData->mPrimaryCursorPosition, cursorOffset, numberOfCharacters );
468
469   if( !impl.IsShowingPlaceholderText() )
470   {
471     // Delete at current cursor position
472     Vector<Character>& currentText = logicalModel->mText;
473     CharacterIndex& oldCursorIndex = eventData->mPrimaryCursorPosition;
474
475     CharacterIndex cursorIndex = 0;
476
477     // Validate the cursor position & number of characters
478     if( ( static_cast< int >( eventData->mPrimaryCursorPosition ) + cursorOffset ) >= 0 )
479     {
480       cursorIndex = eventData->mPrimaryCursorPosition + cursorOffset;
481     }
482
483     if( ( cursorIndex + numberOfCharacters ) > currentText.Count() )
484     {
485       numberOfCharacters = currentText.Count() - cursorIndex;
486     }
487
488     TextUpdateInfo& textUpdateInfo = impl.mTextUpdateInfo;
489
490     if( eventData->mPreEditFlag || // If the preedit flag is enabled, it means two (or more) of them came together i.e. when two keys have been pressed at the same time.
491         ( ( cursorIndex + numberOfCharacters ) <= textUpdateInfo.mPreviousNumberOfCharacters ) )
492     {
493       // Mark the paragraphs to be updated.
494       if( Layout::Engine::SINGLE_LINE_BOX == impl.mLayoutEngine.GetLayout() )
495       {
496         textUpdateInfo.mCharacterIndex = 0;
497         textUpdateInfo.mNumberOfCharactersToRemove = textUpdateInfo.mPreviousNumberOfCharacters;
498         textUpdateInfo.mNumberOfCharactersToAdd = textUpdateInfo.mPreviousNumberOfCharacters - numberOfCharacters;
499         textUpdateInfo.mClearAll = true;
500       }
501       else
502       {
503         textUpdateInfo.mCharacterIndex = std::min( cursorIndex, textUpdateInfo.mCharacterIndex );
504         textUpdateInfo.mNumberOfCharactersToRemove += numberOfCharacters;
505       }
506
507       // Update the input style and remove the text's style before removing the text.
508
509       if( UPDATE_INPUT_STYLE == type )
510       {
511         InputStyle& eventDataInputStyle = eventData->mInputStyle;
512
513         // Keep a copy of the current input style.
514         InputStyle currentInputStyle;
515         currentInputStyle.Copy( eventDataInputStyle  );
516
517         // Set first the default input style.
518         impl.RetrieveDefaultInputStyle( eventDataInputStyle );
519
520         // Update the input style.
521         logicalModel->RetrieveStyle( cursorIndex, eventDataInputStyle );
522
523         // Compare if the input style has changed.
524         const bool hasInputStyleChanged = !currentInputStyle.Equal( eventDataInputStyle );
525
526         if( hasInputStyleChanged )
527         {
528           const InputStyle::Mask styleChangedMask = currentInputStyle.GetInputStyleChangeMask( eventDataInputStyle );
529           // Queue the input style changed signal.
530           eventData->mInputStyleChangedQueue.PushBack( styleChangedMask );
531         }
532       }
533
534       // If the number of current text and the number of characters to be deleted are same,
535       // it means all texts should be removed and all Preedit variables should be initialized.
536       if( ( currentText.Count() - numberOfCharacters == 0 ) && ( cursorIndex == 0 ) )
537       {
538         impl.ClearPreEditFlag();
539         textUpdateInfo.mNumberOfCharactersToAdd = 0;
540       }
541
542       // Updates the text style runs by removing characters. Runs with no characters are removed.
543       logicalModel->UpdateTextStyleRuns( cursorIndex, -numberOfCharacters );
544
545       // Remove the characters.
546       Vector<Character>::Iterator first = currentText.Begin() + cursorIndex;
547       Vector<Character>::Iterator last  = first + numberOfCharacters;
548
549       if( NULL != impl.mEditableControlInterface )
550       {
551         std::string utf8;
552         Utf32ToUtf8( first, numberOfCharacters, utf8 );
553         impl.mEditableControlInterface->TextDeleted( cursorIndex, numberOfCharacters, utf8 );
554       }
555
556       currentText.Erase( first, last );
557
558       // Cursor position retreat
559       oldCursorIndex = cursorIndex;
560
561       eventData->mScrollAfterDelete = true;
562
563       if( EventData::INACTIVE == eventData->mState )
564       {
565         impl.ChangeState( EventData::EDITING );
566       }
567
568       DALI_LOG_INFO( gLogFilter, Debug::General, "Controller::RemoveText %p removed %d\n", &controller, numberOfCharacters );
569       removed = true;
570     }
571   }
572
573   return removed;
574 }
575
576 bool Controller::TextUpdater::RemoveSelectedText(Controller& controller)
577 {
578   bool textRemoved( false );
579
580   Controller::Impl& impl = *controller.mImpl;
581
582   if( EventData::SELECTING == impl.mEventData->mState )
583   {
584     std::string removedString;
585     impl.RetrieveSelection( removedString, true );
586
587     if( !removedString.empty() )
588     {
589       textRemoved = true;
590       impl.ChangeState( EventData::EDITING );
591     }
592   }
593
594   return textRemoved;
595 }
596
597 void Controller::TextUpdater::ResetText(Controller& controller)
598 {
599   Controller::Impl& impl = *controller.mImpl;
600   LogicalModelPtr& logicalModel = impl.mModel->mLogicalModel;
601
602   // Reset buffers.
603   logicalModel->mText.Clear();
604
605   // Reset the embedded images buffer.
606   logicalModel->ClearEmbeddedImages();
607
608   // We have cleared everything including the placeholder-text
609   impl.PlaceholderCleared();
610
611   impl.mTextUpdateInfo.mCharacterIndex = 0u;
612   impl.mTextUpdateInfo.mNumberOfCharactersToRemove = impl.mTextUpdateInfo.mPreviousNumberOfCharacters;
613   impl.mTextUpdateInfo.mNumberOfCharactersToAdd = 0u;
614
615   // Clear any previous text.
616   impl.mTextUpdateInfo.mClearAll = true;
617
618   // The natural size needs to be re-calculated.
619   impl.mRecalculateNaturalSize = true;
620
621   // The text direction needs to be updated.
622   impl.mUpdateTextDirection = true;
623
624   // Apply modifications to the model
625   impl.mOperationsPending = ALL_OPERATIONS;
626 }
627
628 } // namespace Text
629
630 } // namespace Toolkit
631
632 } // namespace Dali