From ba4ff01e0fa5d8052ae8e711849de5d293e0cd6d Mon Sep 17 00:00:00 2001 From: nakst <> Date: Fri, 24 Sep 2021 09:39:49 +0100 Subject: [PATCH] converter sample application --- README.md | 8 +-- apps/samples/converter.cpp | 135 +++++++++++++++++++++++++++++++++++ apps/samples/converter.ini | 5 ++ apps/{ => samples}/hello.c | 0 apps/{ => samples}/hello.ini | 0 desktop/api.cpp | 13 ++-- desktop/gui.cpp | 27 +++++-- desktop/os.header | 6 +- desktop/text.cpp | 2 +- res/Theme Source.dat | Bin 52965 -> 53047 bytes res/Themes/Theme.dat | Bin 53612 -> 53660 bytes 11 files changed, 177 insertions(+), 19 deletions(-) create mode 100644 apps/samples/converter.cpp create mode 100644 apps/samples/converter.ini rename apps/{ => samples}/hello.c (100%) rename apps/{ => samples}/hello.ini (100%) diff --git a/README.md b/README.md index 4451677..a737c32 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # **Essence** — An Operating System -![Screenshot of the OS running in an emulator, showing File Manager, and the new tab screen.](https://handmade.network/public/media/image/p-essence-s1.png) -![Screenshot of the OS running in an emulator, showing GCC running under the POSIX subsystem.](https://handmade.network/public/media/image/p-essence-s3.png) -![Screenshot of the OS running in an emulator, showing the shutdown dialog.](https://handmade.network/public/media/image/p-essence-s5.png) +![Screenshot of the operating system running in an emulator, showing File Manager, and the new tab screen.](https://handmade.network/public/media/image/p-essence-s1.png) +![Screenshot of the operating system running in an emulator, showing GCC running under the POSIX subsystem.](https://handmade.network/public/media/image/p-essence-s3.png) +![Screenshot of the operating system running in an emulator, showing the shutdown dialog.](https://handmade.network/public/media/image/p-essence-s5.png) ## Support @@ -90,4 +90,4 @@ If you want your project to target Essence, you need to generate the API header Currently supported languages are 'c' (also works for C++), 'zig' and 'odin'. -There is currently no documentation for the API; for examples of how to use the API, consult the standard applications in the `apps/` folder of the source tree. A minimal application is demonstrated in `apps/hello.c` and `apps/hello.ini`. By placing your application's `.ini` file in the `apps/` folder, it will be automatically built by the build system. +There is currently no documentation for the API; for examples of how to use the API, consult the standard applications in the `apps/` folder of the source tree. Minimal sample applications are placed in the `apps/samples` folder. By placing your application's `.ini` file in the `apps/` folder, it will be automatically built by the build system. diff --git a/apps/samples/converter.cpp b/apps/samples/converter.cpp new file mode 100644 index 0000000..459feb2 --- /dev/null +++ b/apps/samples/converter.cpp @@ -0,0 +1,135 @@ +// Include the Essence system header. +#include + +// Define the metrics for panelStack. +// These values are specified in pixels, +// but will be automatically scaled using the UI scaling factor. +const EsStyle stylePanelStack = { + .metrics = { + .mask = ES_THEME_METRICS_INSETS + | ES_THEME_METRICS_GAP_MAJOR, + .insets = ES_RECT_1(20), // Spacing around the contents. + .gapMajor = 15, // Spacing between items. + }, +}; + +// Define the metrics for panelForm. +const EsStyle stylePanelForm = { + .metrics = { + .mask = ES_THEME_METRICS_GAP_MAJOR + | ES_THEME_METRICS_GAP_MINOR, + .gapMajor = 5, // Spacing between columns. + .gapMinor = 8, // Spacing between rows. + }, +}; + +// Global variables. +EsTextbox *textboxRate; +EsTextbox *textboxAmount; +EsTextDisplay *displayResult; + +void ConvertCommand(EsInstance *, EsElement *, EsCommand *) { + // Get the conversion rate and amount to convert from the textboxes. + double rate = EsTextboxGetContentsAsDouble(textboxRate); + double amount = EsTextboxGetContentsAsDouble(textboxAmount); + + // Calculate the result, and format it as a string. + char result[64]; + size_t resultBytes = EsStringFormat(result, sizeof(result), "Result: $%F", rate * amount); + + // Replace the contents of the result textbox. + EsTextDisplaySetContents(displayResult, result, resultBytes); +} + +void _start() { + // We're not using the C standard library, + // so we need to initialise global constructors manually. + _init(); + + while (true) { + // Receive a message from the system. + EsMessage *message = EsMessageReceive(); + + if (message->type == ES_MSG_INSTANCE_CREATE) { + // The system wants us to create an instance of our application. + // Call EsInstanceCreate with the message and application name. + EsInstance *instance = EsInstanceCreate(message, "Converter"); + + // Create a layout panel to draw the window background. + EsPanel *panelRoot = EsPanelCreate( + instance->window, // Make the panel the root element of the window. + ES_CELL_FILL // The panel should fill the window. + | ES_PANEL_V_SCROLL_AUTO // Automatically show a vertical scroll bar if needed. + | ES_PANEL_H_SCROLL_AUTO, // Automatically show a horizontal scroll bar if needed. + ES_STYLE_PANEL_WINDOW_BACKGROUND); // Use the window background style. + panelRoot->cName = "panelRoot"; + + // Create a vertical stack to layout the contents of window. + EsPanel *panelStack = EsPanelCreate( + panelRoot, // Add it to panelRoot. + ES_CELL_H_CENTER // Horizontally center it in panelRoot. + | ES_PANEL_STACK // Use the stack layout. + | ES_PANEL_VERTICAL, // Layout child elements from top to bottom. + &stylePanelStack); + + // Add a second layout panel to panelStack to contain the elements of the form. + EsPanel *panelForm = EsPanelCreate( + panelStack, // Add it to panelStack. + ES_PANEL_TABLE // Use table layout. + | ES_PANEL_HORIZONTAL, // Left to right, then top to bottom. + &stylePanelForm); + + // Set the number of columns for the panelForm's table layout. + EsPanelSetBands(panelForm, 2); + + // Add a text display and textbox for the conversion rate to panelForm. + EsTextDisplayCreate( + panelForm, // Add it to panelForm. It will go in the first column. + ES_CELL_H_RIGHT, // Align it to the right of the column. + ES_STYLE_TEXT_LABEL, // Use the text label style. + "Amount per dollar:"); // The contents of the text display. + textboxRate = EsTextboxCreate( + panelForm, // Add it to panelForm. It will go in the second column. + ES_CELL_H_LEFT); // Align it to the left of the column. + + // Set the keyboard focus on the rate textbox. + EsElementFocus(textboxRate); + + // Add a text display and textbox for the conversion amount to panelForm. + EsTextDisplayCreate(panelForm, ES_CELL_H_RIGHT, ES_STYLE_TEXT_LABEL, "Value to convert ($):"); + textboxAmount = EsTextboxCreate(panelForm, ES_CELL_H_LEFT); + + // We want to add a convert button in the second column of next row. + // But the next element we create will go into the first column, + // so we create a spacer element first. + EsSpacerCreate(panelForm); + + // Create the convert button. + EsButton *buttonConvert = EsButtonCreate( + panelForm, // Add it to the panelForm. It will go in the second column. + ES_CELL_H_LEFT // Align it to the left of the column. + | ES_BUTTON_DEFAULT, // Set it as the default button. Pressing Enter will invoke it. + 0, // Automatically determine the style to use. + "Convert"); // The button's label. + + // Set the command callback for the button. + // This is called when the button is invoked. + // That might be from the user clicking it, + // using keyboard input, or an automation API invoking it. + EsButtonOnCommand(buttonConvert, ConvertCommand); + + // Add a horizontal line below panelForm. + EsSpacerCreate( + panelStack, // Add it to panelStack. + ES_CELL_H_FILL, // Fill the horizontal width of panelStack. + ES_STYLE_SEPARATOR_HORIZONTAL); // Use the horizontal separator style. + + // Add a text display for the conversion result to panelStack. + displayResult = EsTextDisplayCreate(panelStack, ES_CELL_H_LEFT, ES_STYLE_TEXT_LABEL, + "Press \u201CConvert\u201D to update the result."); + + // Keep receiving messages in a loop, + // so the system can handle input messages for the window. + } + } +} diff --git a/apps/samples/converter.ini b/apps/samples/converter.ini new file mode 100644 index 0000000..21f6aa7 --- /dev/null +++ b/apps/samples/converter.ini @@ -0,0 +1,5 @@ +[general] +name=Converter + +[build] +source=apps/converter.cpp diff --git a/apps/hello.c b/apps/samples/hello.c similarity index 100% rename from apps/hello.c rename to apps/samples/hello.c diff --git a/apps/hello.ini b/apps/samples/hello.ini similarity index 100% rename from apps/hello.ini rename to apps/samples/hello.ini diff --git a/desktop/api.cpp b/desktop/api.cpp index 67c223f..0dcd528 100644 --- a/desktop/api.cpp +++ b/desktop/api.cpp @@ -1629,9 +1629,12 @@ void EsUndoClear(EsUndoManager *manager) { } } -void EsUndoPush(EsUndoManager *manager, EsUndoCallback callback, const void *item, size_t itemBytes) { +void EsUndoPush(EsUndoManager *manager, EsUndoCallback callback, const void *item, size_t itemBytes, bool setAsActiveUndoManager) { EsMessageMutexCheck(); - EsInstanceSetActiveUndoManager(manager->instance, manager); + + if (setAsActiveUndoManager) { + EsInstanceSetActiveUndoManager(manager->instance, manager); + } Array *stack = manager->state == UNDO_MANAGER_STATE_UNDOING ? &manager->redoStack : &manager->undoStack; @@ -1651,8 +1654,10 @@ void EsUndoPush(EsUndoManager *manager, EsUndoCallback callback, const void *ite manager->state = UNDO_MANAGER_STATE_NORMAL; } - EsCommandSetDisabled(EsCommandByID(manager->instance, ES_COMMAND_UNDO), !manager->undoStack.Length()); - EsCommandSetDisabled(EsCommandByID(manager->instance, ES_COMMAND_REDO), !manager->redoStack.Length()); + if (((APIInstance *) manager->instance->_private)->activeUndoManager == manager) { + EsCommandSetDisabled(EsCommandByID(manager->instance, ES_COMMAND_UNDO), !manager->undoStack.Length()); + EsCommandSetDisabled(EsCommandByID(manager->instance, ES_COMMAND_REDO), !manager->redoStack.Length()); + } if (manager->instance->undoManager == manager) { InstanceSetModified(manager->instance, true); diff --git a/desktop/gui.cpp b/desktop/gui.cpp index 4844245..868b026 100644 --- a/desktop/gui.cpp +++ b/desktop/gui.cpp @@ -1268,6 +1268,8 @@ EsRectangle UIGetTransitionEffectRectangle(EsRectangle bounds, EsTransitionType } void UIDrawTransitionEffect(EsPainter *painter, EsPaintTarget *sourceSurface, EsRectangle bounds, EsTransitionType type, double progress, bool to) { + // TODO Proper blending in the FADE transition. + if ((type == ES_TRANSITION_FADE_OUT && to) || (type == ES_TRANSITION_FADE_IN && !to)) { return; } @@ -2278,9 +2280,20 @@ void LayoutStackPrimary(EsPanel *panel, EsMessage *message) { int available = horizontal ? hSpace : vSpace; int perPush = LayoutStackDeterminePerPush(panel, available, horizontal ? vSpace : hSpace); + int secondary1 = horizontal ? insets.t : insets.l; + int secondary2 = horizontal ? bounds.b - insets.b : bounds.r - insets.r; + int position = horizontal ? (reverse ? insets.r : insets.l) : (reverse ? insets.b : insets.t); bool anyNonHiddenChildren = false; + if (message->type == ES_MSG_LAYOUT) { + if (!horizontal && panel->scroll.enabled[0]) { + secondary2 += panel->scroll.limit[0]; + } else if (horizontal && panel->scroll.enabled[1]) { + secondary2 += panel->scroll.limit[1]; + } + } + for (uintptr_t i = 0; i < childCount; i++) { EsElement *child = panel->GetChild(i); if (child->flags & (ES_ELEMENT_HIDDEN | ES_ELEMENT_NON_CLIENT)) continue; @@ -2291,7 +2304,7 @@ void LayoutStackPrimary(EsPanel *panel, EsMessage *message) { int width = (child->flags & ES_CELL_H_PUSH) ? perPush : child->GetWidth(vSpace); if (reverse) { - relative = ES_RECT_4(bounds.r - position - width, bounds.r - position, insets.t, bounds.b - insets.b); + relative = ES_RECT_4(bounds.r - position - width, bounds.r - position, secondary1, secondary2); } else { relative = ES_RECT_4(position, position + width, insets.t, bounds.b - insets.b); } @@ -2301,9 +2314,9 @@ void LayoutStackPrimary(EsPanel *panel, EsMessage *message) { int height = (child->flags & ES_CELL_V_PUSH) ? perPush : child->GetHeight(hSpace); if (reverse) { - relative = ES_RECT_4(insets.l, bounds.r - insets.r, bounds.b - position - height, bounds.b - position); + relative = ES_RECT_4(secondary1, secondary2, bounds.b - position - height, bounds.b - position); } else { - relative = ES_RECT_4(insets.l, bounds.r - insets.r, position, position + height); + relative = ES_RECT_4(secondary1, secondary2, position, position + height); } position += height + gap; @@ -3392,7 +3405,7 @@ void EsElementSetCellRange(EsElement *element, int xFrom, int yFrom, int xTo, in element->tableCell = cell; } -void EsPanelSetBands(EsPanel *panel, size_t columnCount, size_t rowCount, EsPanelBand *columns, EsPanelBand *rows) { +void EsPanelSetBands(EsPanel *panel, size_t columnCount, size_t rowCount, const EsPanelBand *columns, const EsPanelBand *rows) { EsMessageMutexCheck(); EsAssert(panel->flags & ES_PANEL_TABLE); // Cannot set the bands layout for a non-table panel. EsHeapFree(panel->bands[0]); @@ -3407,11 +3420,11 @@ void EsPanelSetBands(EsPanel *panel, size_t columnCount, size_t rowCount, EsPane if (rows && panel->bands[1]) EsMemoryCopy(panel->bands[1], rows, rowCount * sizeof(EsPanelBand)); } -void EsPanelSetBandsAll(EsPanel *panel, EsPanelBand *column, EsPanelBand *row) { +void EsPanelSetBandsAll(EsPanel *panel, const EsPanelBand *column, const EsPanelBand *row) { EsMessageMutexCheck(); EsAssert(panel->flags & ES_PANEL_TABLE); // Cannot set the bands layout for a non-table panel. - EsPanelBand *templates[2] = { column, row }; + const EsPanelBand *templates[2] = { column, row }; for (uintptr_t axis = 0; axis < 2; axis++) { if (!templates[axis]) continue; @@ -3884,7 +3897,7 @@ EsButton *EsButtonCreate(EsElement *parent, uint64_t flags, const EsStyle *style } EsButtonSetCheck(button, (EsCheckState) (flags & 3), false); - + button->MaybeRefreshStyle(); return button; } diff --git a/desktop/os.header b/desktop/os.header index 6cec87c..1bd49b3 100644 --- a/desktop/os.header +++ b/desktop/os.header @@ -2291,7 +2291,7 @@ function void EsUndoEndGroup(EsUndoManager *manager); function void EsUndoInvokeGroup(EsUndoManager *manager, bool redo); function bool EsUndoPeek(EsUndoManager *manager, EsUndoCallback *callback, const void **item); function void EsUndoPop(EsUndoManager *manager); -function void EsUndoPush(EsUndoManager *manager, EsUndoCallback callback, const void *item, size_t itemBytes); +function void EsUndoPush(EsUndoManager *manager, EsUndoCallback callback, const void *item, size_t itemBytes, bool setAsActiveUndoManager = true); function bool EsUndoInUndo(EsUndoManager *manager); function bool EsUndoIsEmpty(EsUndoManager *manager, bool redo); function ES_INSTANCE_TYPE *EsUndoGetInstance(EsUndoManager *manager); @@ -2440,8 +2440,8 @@ function EsElement *EsSpacerCreate(EsElement *parent, uint64_t flags = ES_FLAGS_ function EsSplitter *EsSplitterCreate(EsElement *parent, uint64_t flags = ES_FLAGS_DEFAULT, const EsStyle *style = ES_NULL); function EsCanvasPane *EsCanvasPaneCreate(EsElement *parent, uint64_t flags = ES_FLAGS_DEFAULT, const EsStyle *style = ES_NULL); -function void EsPanelSetBands(EsPanel *panel, size_t columnCount, size_t rowCount = 0, EsPanelBand *columns = ES_NULL, EsPanelBand *rows = ES_NULL); -function void EsPanelSetBandsAll(EsPanel *panel, EsPanelBand *column = ES_NULL, EsPanelBand *row = ES_NULL); // Set all the columns/rows to have the same properties. This must be called after the final number of bands has been determined/set! +function void EsPanelSetBands(EsPanel *panel, size_t columnCount, size_t rowCount = 0, const EsPanelBand *columns = ES_NULL, const EsPanelBand *rows = ES_NULL); +function void EsPanelSetBandsAll(EsPanel *panel, const EsPanelBand *column = ES_NULL, const EsPanelBand *row = ES_NULL); // Set all the columns/rows to have the same properties. This must be called after the final number of bands has been determined/set! function void EsPanelTableSetChildCells(EsPanel *panel); // Automatically set the child cells for items in a table. This is only necessary if the number of columns/rows is changed after adding items to a table. function void EsPanelSwitchTo(EsPanel *panel, EsElement *targetChild, EsTransitionType transitionType, uint32_t flags = ES_FLAGS_DEFAULT, float timeMultiplier = 1); // TODO More customization of transitions? diff --git a/desktop/text.cpp b/desktop/text.cpp index b355364..640846a 100644 --- a/desktop/text.cpp +++ b/desktop/text.cpp @@ -3960,7 +3960,7 @@ void EsTextboxInsert(EsTextbox *textbox, const char *string, ptrdiff_t stringByt } undoItem->textbox = textbox; - EsUndoPush(textbox->undo, TextboxUndoItemCallback, undoItem, undoItemBytes); + EsUndoPush(textbox->undo, TextboxUndoItemCallback, undoItem, undoItemBytes, false /* do not set instance's undo manager */); } EsHeapFree(undoItem); diff --git a/res/Theme Source.dat b/res/Theme Source.dat index b780ddbe03678a0b8fe0af2d3a27ff8866c9d64e..5b45997c43519b94c21f06b8cb21abec6e9f68e8 100644 GIT binary patch delta 109 zcmaDlmwEd<<_#rsjLnntlqDzk$r(<5kk87)@~@*|YKsH|1H-1)$!nG6CnwZ$O};N@ z!q~C7ZgDT;8<5uwWV1l@y`LjC`G5(}0NMYyo&)8b1F&=+lfNSxla3xSlfRZ0lkXlrlf0WQv;CuO0kR=Cu2k?5i~s-t diff --git a/res/Themes/Theme.dat b/res/Themes/Theme.dat index 54ec1bc95103db29aded9798cad33afdf5c3bd89..d6b9544ed934a6fd14f133013acd675551a34e8d 100644 GIT binary patch delta 3958 zcmZ9P4@}k9701uL?>%@PZA~9W zmy*f`?y!cXY=Mp?B(=`w*KI6$i!`H&OKFRVu{MQOQns=bH%x7Y8N_`rkB9HwANjs} z@8_J~x#ymHf4`qjzGLe8t*Q2fD&JeL5m5qE?Ig0kOeFB9E-x|=B2M1^3Vm`!A{n8N~-mtA__ z=BS3@h8DmD_O8mxM5ciAF|cJ}W}rvepJgFzj#gM4Vg7v8^y=yW`vTL2g;~M_5xT*? z1he7_u*+PI2>q~pr6ueG8$({f-h*IYn=NVrp(3ct+~yucxC6_#+A1c%?rLlb>^~Zt z=6k|sp)*AH_?)zP!^1*LXNBcEh z;$xg^vN_zaRB5&FfGyS{oCRAF#MHVk@Hs?ihowi0-2oP@d3S-0YOI%UAVME3OXr`D zKr|K}nADV63%uPfYS`JpQ)f$Tc%kr;Qs!K(P1$&x%EVhlgns+KJYc-Y* zwhj@3pF2C(T5OI2SZ-+96@qQlA{2w=Ykpv| zpjqyN?b0I5fK_SC{0fm%V-a9`G!_H4S7UKt`z&(5%4A`osen81QVrQIux%kni>LtX z#gJtSQ4Cfa!~~w0sbJW0cD9EB_YykUOOUVsZ@BgSQWYo-Kh;RXxEbY;60Xwc8 zmR7JfjkR;G)#m7c<%DMG1Usd%F0j)Y>*jNa(5t+)9QwebHFg>73|1T1YV~;b^9@88 zgyl8OG7R=nP#u)_ZLl|j>xD{=bMhkFp-EVNr)4(<=Cy>WqRv-Mko8rfzi5?=1v_U6 z5Am~uo!8>!8NXVT{Mbr-w#iI&b#)Jm(c*e&i4wF1pL@mCILY`Vc9B?z4?nGJQy5N})YjcktND`^Q_ z9Oshuc>Z+7Q{4PS3{S+_MY|CnZ1GOEOQ=h@e#Xy@coCkp*a=~`)i&bU&#-h|NqC~K1w ztn;Vy9@`2skB{_M2R00oF<%vob&V0=6a2leJ?>V0ly~es~l;93m?L8%vY8E^Z zx#q7z=IvM08ngS(OBqqi`wt!!F5|9irNFA94_uR}2l>jOH?iD(2Var}b;iG4DFUmy zTX#*;;W^V~W8-1CU*&!EYsENUs=ug?PXkbgvF*q+LQES!J$hcEhKr7WDf1fVZW`nh zjTc3kG26IH2)x&e7>^nlC7yI4uJh^B_1@k* zqC7WI4b-v*Pb^f8*Xsw+6m%P!c?xVb(N$=23sE=5nd^xzLoLuSl(~VZ7b3(PsYFwS zn5S`%i>RalkL3X(_YtDjgLsP`Mq5ubP)`*!@REPBzNd-IIIJB|Ul#h==tDj=PQkZm z7XpF%z+&NhzXZILs0-mWSVad4;$7D565V|5{9!(g5W#l0BR;3!GzmF(t84`H{i9pAkV43(Y4Rjf@X2FJJhw;YpJdPH+i3R1L zt{0+^9JDo(s0%vl!G1x$!XKj+Maa4ktAwuF5CodW%`q6qxUrIGqC$*S@R{e3=wn1f zP>l+R$-1D47vRIuO^MzH^ai2ZaoB~;P&~Y1QBz3mjC;kS{tWun@EdX<@_fH!)1&@S zeo(&v%h-#VP=aHSY73CLidj>&Fh)HA>UJ-Hxp8I^mWK@RcZ4AF-+9IIq~Ktr&Mf!?_r*felHtP*Ur5Op3M{ W?KIR4zgF0imLoZ6*oc00uk-&Bac6J< delta 3914 zcmZ9P4NO#57RT?s^Jc&m6+2i^8NP9`0)mVMg~4$Jr7g64g9Rz*AYj3Q4wbfS2O>?` zIxX14ZtO1G;I2*Flw@rl&9VtiY1VGol%}{LDVrkIl(Z!6W@*Z1NtUkrzYGI2_nANU zzTY|T+;h)8?}3BAH+=UyL*3J5W50TdF%|(;Rxmcx&KSo(dAV@OaTct}XpmDDf`9T_ z0Q*=It8?kcaV>&j92J8pyA}Hu5TfMuO2m2-JWx(alYZ2@>#*I^yNiT!_TUni5 zV(dQ}-zx3vZK2UsVYUUq^k2n21nfs8%`mVBK7O(+QFI>>5@0d-Bq1#+U`B;yfCU)+ zH=0GM2D8l$OPNxF9I(wwgnY0qK1}YWl)4e27MANuYzNqO#k(2ouELyj2N618*|GML zcuU>`7OHr=!FDQafTA_C?HVi=C5J(j)67# zd2@CfEZ)aY)?t!9(#)=DSoSJ8%z*7v*eqC*XSYILGQtAbekJN6SgP{u5?GqTmcb4h z4O{)*MUCo=Xs`~v`%2glutQ3SFtAL;FN)qngalZQ_*pnh0sF+C@eHt|${UhH++?;D z!jh|4s=-bu2i^$wn8KV?ga}=*6et!q*ynyZFg5`86uzO}w^SDP23*mq#3jG{(nvi|Nb0iM8z*7wt?y@2xhbsA$qV9)q% zoAWTRIv>XIO2^UF0JAL-mUe*v(c8?N*_p;14#$H$jJO^I>vzL8aoTfw7cE+XDP>na@28*d`hgEp+wN~f zVK~jjM^Hz2tqy)}X+Vg`7tekwMT^I>``?vKJAzq&|cyi-ie>DOXrkN(#^ z$wCC~rMa+VJ#Md6@Iw9MzITOKNzzTp4{;t$JNJJZxU!$I741hO*<1en<~EJC#9HX< zls9lT?Wup_c0K99=UgnO(c1?@$zpv~T(hoDKd^qu`}N-*?BhI6PfCw8a5ohkc8Q=& zdh+ng!o521Ao?`ZCEQt($(t+}+~iNpZ7!Ze1=)RKI$NL1t}*cxvgKy-R(c_~kK1X} z@k@M^x{rUx^Qh=VIv=NBocIh&p3J)>#Jwl`Eke=GQc!W8=qmP<9n-PmwS5H8Lh-B^EupP#ukjqcsC2XJ##3b%`2^i5s}SMk zl%0E6^q155&_sH({J5AZr<&jhS}e=pagz~g{{Fek0yR9;@`VUIzxvPs zT|a+?N9jq=m2mE)?$#@u(U!I=I0O09O7RzTF?m7OKvZ5_9p+aDz0;Y%`89f{y}{+o zU@Wkeu|%ly0Nyw#0iVYnXdJo;O+Es)kFhRj>mac&iBCgr}*?IU`1$juV-vryAMGrBwvPKDg1h$ zgJPMB4FJr?V$TSiSh%s^EEZ!(y;`ao>7PPvLhzt1NI_ZF=F10=TpAj~>O%3Ys2|je z1DgiyfaG}NXXqk34h9QMqQS>enW?8>N9Sora86J-=J^-EaX4`}xZY?_L(F58b8ml7 zq0hbZk&{Re6;D8{9tVC5Ly2%sL|0{$Ak!U4C=cE87=l3epuERXx(db?A!{X)Zuj`* gp`