From a0b814bc17db4e1fa86e6f953706f988f1e5f418 Mon Sep 17 00:00:00 2001 From: Jay Dolan Date: Thu, 24 Sep 2026 22:04:31 -0400 Subject: [PATCH 01/11] Let JSON layouts declare a ScrollView content view ScrollView binds "contentView" from JSON and installs it with setContentView, as Panel does for its accessory view. ScrollView also overrides View::init, so a ScrollView that JSON creates gets its scroll bar. Before this, only the initWithFrame path created one, and a JSON ScrollView asserted on its first content view. Co-Authored-By: Claude Opus 5.5 (1M context) --- Sources/ObjectivelyMVC/ScrollView.c | 16 ++++++++++++++++ Sources/ObjectivelyMVC/ScrollView.h | 2 ++ 2 files changed, 18 insertions(+) diff --git a/Sources/ObjectivelyMVC/ScrollView.c b/Sources/ObjectivelyMVC/ScrollView.c index b5e7b770..417ee1f9 100644 --- a/Sources/ObjectivelyMVC/ScrollView.c +++ b/Sources/ObjectivelyMVC/ScrollView.c @@ -53,12 +53,20 @@ static void awakeWithDictionary(View *self, const Dictionary *dictionary) { ScrollView *this = (ScrollView *) self; + View *contentView = NULL; + const Inlet inlets[] = MakeInlets( + MakeInlet("contentView", InletTypeView, &contentView, NULL), MakeInlet("scrollbarVisibility", InletTypeEnum, &this->scrollBarVisibility, (ident) ScrollBarVisibilityNames), MakeInlet("step", InletTypeFloat, &this->step, NULL) ); $(self, bind, inlets, dictionary); + + if (contentView) { + $(this, setContentView, contentView); + release(contentView); + } } /** @@ -79,6 +87,13 @@ static void applyStyle(View *self, const Style *style) { $(self, setNeedsLayout); } +/** + * @see View::init(View *) + */ +static View *init(View *self) { + return (View *) $((ScrollView *) self, initWithFrame, NULL); +} + /** * @see View::layoutSubviews(View *) */ @@ -259,6 +274,7 @@ static void initialize(Class *clazz) { ((ViewInterface *) clazz->interface)->applyStyle = applyStyle; ((ViewInterface *) clazz->interface)->awakeWithDictionary = awakeWithDictionary; + ((ViewInterface *) clazz->interface)->init = init; ((ViewInterface *) clazz->interface)->layoutSubviews = layoutSubviews; ((ControlInterface *) clazz->interface)->captureEvent = captureEvent; diff --git a/Sources/ObjectivelyMVC/ScrollView.h b/Sources/ObjectivelyMVC/ScrollView.h index b92e2d57..629e3fe9 100644 --- a/Sources/ObjectivelyMVC/ScrollView.h +++ b/Sources/ObjectivelyMVC/ScrollView.h @@ -78,6 +78,8 @@ struct ScrollView { /** * @brief The content View. + * @remarks A JSON layout MAY declare it as `"contentView": { ... }`, which is the same as + * ScrollView::setContentView with the resulting View. */ View *contentView; From 9ed91d697ebd8024ff556389bb2376ca2b81fdd3 Mon Sep 17 00:00:00 2001 From: Jay Dolan Date: Thu, 24 Sep 2026 22:04:31 -0400 Subject: [PATCH 02/11] Add a stretch attribute for StackView subviews A StackView gives each subview the size of its content along its axis, and the fill distributions scale every subview alike. A subview styled with stretch now shares whatever space the StackView's bounds leave, so that one child, such as a page beneath a row of tabs, can fill a StackView that its superview sizes. Existing layouts do not change. Co-Authored-By: Claude Opus 5.5 (1M context) --- Sources/ObjectivelyMVC/StackView.c | 24 ++++++ Sources/ObjectivelyMVC/View.c | 3 +- Sources/ObjectivelyMVC/View.h | 11 +++ Tests/ObjectivelyMVC/View.c | 130 +++++++++++++++++++++++++++++ 4 files changed, 167 insertions(+), 1 deletion(-) diff --git a/Sources/ObjectivelyMVC/StackView.c b/Sources/ObjectivelyMVC/StackView.c index 19beb92d..4fa3629d 100644 --- a/Sources/ObjectivelyMVC/StackView.c +++ b/Sources/ObjectivelyMVC/StackView.c @@ -110,6 +110,17 @@ static void layoutSubviews(View *self) { } } + int numStretch = 0; + for (size_t i = 0; i < subviews->count; i++) { + const View *subview = $(subviews, objectAtIndex, i); + if (subview->stretch) { + numStretch++; + } + } + + int remaining = this->distribution == StackViewDistributionDefault && numStretch ? + max(availableSize - requestedSize, 0) : 0; + int pos = 0; const float scale = requestedSize ? availableSize / (float) requestedSize : 1.f; @@ -173,6 +184,19 @@ static void layoutSubviews(View *self) { break; } + if (subview->stretch && remaining) { + const int share = --numStretch ? remaining / (numStretch + 1) : remaining; + switch (this->axis) { + case StackViewAxisVertical: + subviewSize.h += share; + break; + case StackViewAxisHorizontal: + subviewSize.w += share; + break; + } + remaining -= share; + } + $(subview, layoutWithSize, &subviewSize); // Align along secondary axis diff --git a/Sources/ObjectivelyMVC/View.c b/Sources/ObjectivelyMVC/View.c index c1489d7a..67ad8a9f 100644 --- a/Sources/ObjectivelyMVC/View.c +++ b/Sources/ObjectivelyMVC/View.c @@ -302,6 +302,7 @@ static void applyStyle(View *self, const Style *style) { MakeInlet("padding-bottom", InletTypeInteger, &self->padding.bottom, NULL), MakeInlet("padding-left", InletTypeInteger, &self->padding.left, NULL), MakeInlet("pointer-events", InletTypeEnum, &self->pointerEvents, (ident) ViewPointerEventsNames), + MakeInlet("stretch", InletTypeBool, &self->stretch, NULL), MakeInlet("top", InletTypeInteger, &self->frame.y, NULL), MakeInlet("visibility", InletTypeEnum, &self->visibility, (ident) ViewVisibilityNames), MakeInlet("width", InletTypeInteger, &self->frame.w, NULL) @@ -1710,7 +1711,7 @@ static void resize(View *self, const SDL_Size *size) { $(self, setNeedsLayout); if (self->superview) { - if ($(self->superview, isContainer) || self->alignment & ViewAlignmentMaskAny) { + if ($(self->superview, isContainer) || self->alignment & ViewAlignmentMaskAny || self->stretch) { $(self->superview, setNeedsLayout); } } diff --git a/Sources/ObjectivelyMVC/View.h b/Sources/ObjectivelyMVC/View.h index 1f8c4118..35421ccf 100644 --- a/Sources/ObjectivelyMVC/View.h +++ b/Sources/ObjectivelyMVC/View.h @@ -424,6 +424,17 @@ struct View { */ Style *style; + /** + * @brief Whether this View takes a share of the space its StackView leaves along its axis. + * @remarks A StackView gives each subview the size of its content along its axis. The subviews + * that stretch share whatever remains of the StackView's bounds, so that one child, such as a + * page beneath a row of tabs, can fill a StackView that is itself sized by its superview. + * Outside of a StackView, this has no effect. Styled as `stretch`. + * @see StackView::layoutSubviews(View *) + * @styled + */ + bool stretch; + /** * @brief An optional Stylesheet. * @remarks If set, this Stylesheet is added to or removed from the current Theme when this diff --git a/Tests/ObjectivelyMVC/View.c b/Tests/ObjectivelyMVC/View.c index 14720592..cced926b 100644 --- a/Tests/ObjectivelyMVC/View.c +++ b/Tests/ObjectivelyMVC/View.c @@ -355,6 +355,131 @@ START_TEST(pointerEventsPassThroughToSubviewsAndSiblings) { } END_TEST +/** + * @brief Creates a View of the given size that a StackView may resize. + */ +static View *flexibleView(int w, int h) { + + View *view = $(alloc(View), initWithFrame, &MakeRect(0, 0, w, h)); + + view->autoresizingMask = ViewAutoresizingNone; + + return view; +} + +START_TEST(stretchChildTakesRemainingHeight) { + + StackView *stackView = $(alloc(StackView), initWithFrame, NULL); + stackView->view.minSize = stackView->view.maxSize = MakeSize(100, 200); + + View *tabs = fixedView(100, 20); + View *page = flexibleView(100, 30); + page->stretch = true; + + $((View *) stackView, addSubview, tabs); + $((View *) stackView, addSubview, page); + + $((View *) stackView, layoutIfNeeded); + + ck_assert_int_eq(200, stackView->view.frame.h); + ck_assert_int_eq(20, tabs->frame.h); + ck_assert_int_eq(20, page->frame.y); + ck_assert_int_eq(180, page->frame.h); + + release(stackView); + +} END_TEST + +START_TEST(stretchChildrenShareRemainingWidth) { + + StackView *stackView = $(alloc(StackView), initWithFrame, NULL); + stackView->view.minSize = stackView->view.maxSize = MakeSize(111, 10); + stackView->axis = StackViewAxisHorizontal; + + View *a = fixedView(10, 10); + View *b = flexibleView(0, 10); + View *c = flexibleView(0, 10); + b->stretch = c->stretch = true; + + $((View *) stackView, addSubview, a); + $((View *) stackView, addSubview, b); + $((View *) stackView, addSubview, c); + + $((View *) stackView, layoutIfNeeded); + + ck_assert_int_eq(10, a->frame.w); + ck_assert_int_eq(50, b->frame.w); + ck_assert_int_eq(10, b->frame.x); + ck_assert_int_eq(51, c->frame.w); + ck_assert_int_eq(60, c->frame.x); + + release(stackView); + +} END_TEST + +START_TEST(stretchIsIgnoredWithoutRemainingSpace) { + + StackView *stackView = $(alloc(StackView), initWithFrame, NULL); + + View *a = fixedView(40, 20); + View *b = flexibleView(40, 30); + b->stretch = true; + + $((View *) stackView, addSubview, a); + $((View *) stackView, addSubview, b); + + $((View *) stackView, layoutIfNeeded); + + ck_assert_int_eq(50, stackView->view.frame.h); + ck_assert_int_eq(30, b->frame.h); + + release(stackView); + +} END_TEST + +START_TEST(stretchContainChildKeepsStretchedSize) { + + StackView *stackView = $(alloc(StackView), initWithFrame, NULL); + stackView->view.minSize = stackView->view.maxSize = MakeSize(100, 200); + + View *tabs = fixedView(100, 20); + + StackView *page = $(alloc(StackView), initWithFrame, NULL); + page->view.stretch = true; + $((View *) page, addSubview, fixedView(80, 40)); + + $((View *) stackView, addSubview, tabs); + $((View *) stackView, addSubview, (View *) page); + + $((View *) stackView, layoutIfNeeded); + + ck_assert_int_eq(180, page->view.frame.h); + + $((View *) page, setNeedsLayout); + $((View *) stackView, layoutIfNeeded); + + ck_assert_int_eq(180, page->view.frame.h); + + release(stackView); + +} END_TEST + +START_TEST(scrollViewContentViewFromJSON) { + + View *view = $$(View, viewWithCharacters, + "{ \"class\": \"ScrollView\", \"contentView\": { \"identifier\": \"content\" } }", NULL); + ck_assert_ptr_nonnull(view); + + ScrollView *scrollView = (ScrollView *) view; + ck_assert_ptr_nonnull(scrollView->contentView); + ck_assert_str_eq("content", scrollView->contentView->identifier); + ck_assert_ptr_eq(view, scrollView->contentView->superview); + ck_assert($(scrollView->contentView, hasClassName, "contentView")); + + release(view); + +} END_TEST + int main(int argc, char **argv) { TCase *tcase = tcase_create("View"); @@ -369,6 +494,11 @@ int main(int argc, char **argv) { tcase_add_test(tcase, setVisibilityMarksSuperviewNeedsLayout); tcase_add_test(tcase, alignedContainChildRecentersOnGrowth); tcase_add_test(tcase, pointerEventsPassThroughToSubviewsAndSiblings); + tcase_add_test(tcase, stretchChildTakesRemainingHeight); + tcase_add_test(tcase, stretchChildrenShareRemainingWidth); + tcase_add_test(tcase, stretchIsIgnoredWithoutRemainingSpace); + tcase_add_test(tcase, stretchContainChildKeepsStretchedSize); + tcase_add_test(tcase, scrollViewContentViewFromJSON); Suite *suite = suite_create("View"); suite_add_tcase(suite, tcase); From d094c678bc7182c07c0c811db325cec632e2f3e4 Mon Sep 17 00:00:00 2001 From: Jay Dolan Date: Thu, 24 Sep 2026 23:19:57 -0400 Subject: [PATCH 03/11] Lay out the superview when a style changes a view's visibility View::setVisibility asks the superview for layout, but a visibility that a Stylesheet sets is bound directly by View::applyStyle, which did not. A view that a class change made visible drew at its stale frame, over its siblings, until something else laid out its StackView. Co-Authored-By: Claude Opus 5.5 (1M context) --- Sources/ObjectivelyMVC/View.c | 6 ++++++ Tests/ObjectivelyMVC/View.c | 30 ++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/Sources/ObjectivelyMVC/View.c b/Sources/ObjectivelyMVC/View.c index 67ad8a9f..14e860df 100644 --- a/Sources/ObjectivelyMVC/View.c +++ b/Sources/ObjectivelyMVC/View.c @@ -308,8 +308,14 @@ static void applyStyle(View *self, const Style *style) { MakeInlet("width", InletTypeInteger, &self->frame.w, NULL) ); + const ViewVisibility visibility = self->visibility; + $(self, bind, inlets, style->attributes); + if (self->visibility != visibility && self->superview) { + $(self->superview, setNeedsLayout); + } + if ((self->alignment & ViewAlignmentMaskHorizontal) && $(style->attributes, objectForKeyPath, "left")) { $(self, warn, WarningTypeStyle, "`left` is overruled by the horizontal `alignment`"); diff --git a/Tests/ObjectivelyMVC/View.c b/Tests/ObjectivelyMVC/View.c index cced926b..ea0ab85b 100644 --- a/Tests/ObjectivelyMVC/View.c +++ b/Tests/ObjectivelyMVC/View.c @@ -464,6 +464,35 @@ START_TEST(stretchContainChildKeepsStretchedSize) { } END_TEST +START_TEST(styledVisibilityRelaysOutSuperview) { + + StackView *stackView = $(alloc(StackView), initWithFrame, NULL); + + View *a = fixedView(40, 20); + View *b = fixedView(40, 30); + b->visibility = ViewVisibilityHidden; + + $((View *) stackView, addSubview, a); + $((View *) stackView, addSubview, b); + + $((View *) stackView, layoutIfNeeded); + + ck_assert_int_eq(20, stackView->view.frame.h); + + Style *style = $(alloc(Style), initWithAttributes, NULL); + $(style, addEnumAttribute, "visibility", ViewVisibilityNames, ViewVisibilityVisible); + $(b, applyStyle, style); + release(style); + + $((View *) stackView, layoutIfNeeded); + + ck_assert_int_eq(50, stackView->view.frame.h); + ck_assert_int_eq(20, b->frame.y); + + release(stackView); + +} END_TEST + START_TEST(scrollViewContentViewFromJSON) { View *view = $$(View, viewWithCharacters, @@ -498,6 +527,7 @@ int main(int argc, char **argv) { tcase_add_test(tcase, stretchChildrenShareRemainingWidth); tcase_add_test(tcase, stretchIsIgnoredWithoutRemainingSpace); tcase_add_test(tcase, stretchContainChildKeepsStretchedSize); + tcase_add_test(tcase, styledVisibilityRelaysOutSuperview); tcase_add_test(tcase, scrollViewContentViewFromJSON); Suite *suite = suite_create("View"); From 43fca3e44ffef093386a92af8aa05e457d75efa1 Mon Sep 17 00:00:00 2001 From: Jay Dolan Date: Thu, 24 Sep 2026 23:36:34 -0400 Subject: [PATCH 04/11] Style the TabViewController view with a class, not its element style The view's contain mask was set in its element style, which outranks every Stylesheet, so no layout could make it fill its superview. It now wears the tabViewController class, which the default stylesheet sizes to contain. Co-Authored-By: Claude Opus 5.5 (1M context) --- Assets/stylesheet.css | 4 ++++ Sources/ObjectivelyMVC/TabViewController.c | 3 +-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Assets/stylesheet.css b/Assets/stylesheet.css index e7a2c21c..8621e6db 100644 --- a/Assets/stylesheet.css +++ b/Assets/stylesheet.css @@ -364,6 +364,10 @@ TabView { spacing: 4; } +.tabViewController { + autoresizing-mask: contain; +} + TabView > .tabPageView { border-color: silver; border-radius: 4; diff --git a/Sources/ObjectivelyMVC/TabViewController.c b/Sources/ObjectivelyMVC/TabViewController.c index 5c9280cf..70d7ae32 100644 --- a/Sources/ObjectivelyMVC/TabViewController.c +++ b/Sources/ObjectivelyMVC/TabViewController.c @@ -50,8 +50,7 @@ static void loadView(ViewController *self) { super(ViewController, self, loadView); - $(self->view->style, addEnumAttribute, "autoresizing-mask", ViewAutoresizingNames, - ViewAutoresizingContain); + $(self->view, addClassName, "tabViewController"); TabViewController *this = (TabViewController *) self; From d52adf15be4fdea80dafa283c8bd14df4aa80f02 Mon Sep 17 00:00:00 2001 From: Jay Dolan Date: Thu, 24 Sep 2026 23:36:34 -0400 Subject: [PATCH 05/11] Let TextView auto-complete with Tab TextViewDelegate::completionsForPrefix returns the completions for the text before the cursor. Tab completes to the longest prefix they share, and then Tab and Shift+Tab cycle through them. A lone completion is not cycled, so the next Tab asks again, e.g. for a completed directory's contents. Editing, moving the cursor or losing focus ends cycling. When there is nothing to complete, Tab ends editing and advances the key responder as before. Co-Authored-By: Claude Opus 5.5 (1M context) --- Sources/ObjectivelyMVC/TextView.c | 122 +++++++++++++++++++++- Sources/ObjectivelyMVC/TextView.h | 27 +++++ Sources/ObjectivelyMVC/WindowController.c | 25 +++-- 3 files changed, 164 insertions(+), 10 deletions(-) diff --git a/Sources/ObjectivelyMVC/TextView.c b/Sources/ObjectivelyMVC/TextView.c index df1ea0ec..b8c17e06 100644 --- a/Sources/ObjectivelyMVC/TextView.c +++ b/Sources/ObjectivelyMVC/TextView.c @@ -51,6 +51,8 @@ static void dealloc(Object *self) { release(this->attributedText); + release(this->completions); + super(Object, self, dealloc); } @@ -242,6 +244,103 @@ static size_t unitLengthBefore(const char *chars, size_t len, size_t position, c return unit; } +/** + * @brief Replaces the text before the cursor with `chars`, leaving the cursor after them. + */ +static void replacePrefix(TextView *self, const char *chars) { + + const Range range = { .location = 0, .length = self->position }; + $(self->attributedText, replaceCharactersInRange, range, chars); + + self->position = strlen(chars); +} + +/** + * @brief Auto-completes the text before the cursor. + * @details The first Tab completes to the longest prefix the delegate's completions share, and + * then Tab and Shift+Tab cycle through them. A lone completion is not cycled, so that the next + * Tab asks again, e.g. for the contents of a completed directory. + * @param step 1 to cycle forward, -1 to cycle backward. + * @return True if the text was completed, false if Tab should advance the key responder. + */ +static bool autocomplete(TextView *self, int step, const ImageAtlas *icons) { + + if (self->completions) { + const size_t count = self->completions->count; + + if (step > 0) { + self->completion = self->completion == count ? 0 : (self->completion + 1) % count; + } else { + self->completion = self->completion == 0 || self->completion == count ? count - 1 : self->completion - 1; + } + + const String *completion = $(self->completions, objectAtIndex, self->completion); + replacePrefix(self, completion->chars ?: ""); + return true; + } + + if (self->delegate.completionsForPrefix == NULL) { + return false; + } + + String *prefix = $(self->attributedText, substring, (Range) { .length = self->position }); + Array *completions = self->delegate.completionsForPrefix(self, prefix->chars ?: ""); + + bool didComplete = false; + + if (completions && completions->count) { + const size_t count = completions->count; + const String *first = $(completions, objectAtIndex, 0); + + size_t len = first->length; + for (size_t i = 1; i < count; i++) { + const String *other = $(completions, objectAtIndex, i); + size_t j = 0; + while (j < len && j < other->length && first->chars[j] == other->chars[j]) { + j++; + } + len = j; + } + + // Never stop inside an escape or a UTF-8 encoded character + size_t shared = 0; + while (shared < len) { + const size_t unit = unitLengthAt(first->chars, first->length, shared, icons); + if (shared + unit > len) { + break; + } + shared += unit; + } + + if (shared > prefix->length) { + String *common = $(first, substring, (Range) { .length = shared }); + replacePrefix(self, common->chars); + release(common); + + self->completion = count; + didComplete = true; + } else if (count > 1) { + self->completion = step > 0 ? 0 : count - 1; + + const String *completion = $(completions, objectAtIndex, self->completion); + replacePrefix(self, completion->chars ?: ""); + didComplete = true; + } else if (strcmp(first->chars ?: "", prefix->chars ?: "")) { + replacePrefix(self, first->chars ?: ""); + didComplete = true; + } + + if (count > 1) { + self->completions = retain(completions); + } + } + + release(completions); + release(prefix); + + return didComplete; +} + #pragma mark - Control /** @@ -249,12 +348,14 @@ static size_t unitLengthBefore(const char *chars, size_t len, size_t position, c */ static bool captureEvent(Control *self, const SDL_Event *event) { - bool didEdit = false, didCaptureEvent = false; + bool didEdit = false, didComplete = false, didCaptureEvent = false; View *view = (View *) self; TextView *this = (TextView *) self; + const size_t position = this->position; + if (this->isEditable) { if (event->type == SDL_EVENT_TEXT_INPUT) { if ($(self, isFocused)) { @@ -278,11 +379,18 @@ static bool captureEvent(Control *self, const SDL_Event *event) { switch (event->key.key) { + case SDLK_TAB: + case SDLK_KP_TAB: + if (autocomplete(this, (event->key.mod & SDL_KMOD_SHIFT) ? -1 : 1, icons)) { + didEdit = didComplete = true; + } else { + $(view, resignKeyResponder); + } + break; + case SDLK_ESCAPE: case SDLK_KP_ENTER: case SDLK_RETURN: - case SDLK_TAB: - case SDLK_KP_TAB: $(view, resignKeyResponder); break; @@ -373,6 +481,10 @@ static bool captureEvent(Control *self, const SDL_Event *event) { } } + if (!didComplete && (didEdit || this->position != position)) { + this->completions = release(this->completions); + } + if (didEdit) { $((View *) self, setNeedsLayout); if (this->delegate.didEdit) { @@ -408,6 +520,8 @@ static void stateDidChange(Control *self) { SDL_StopTextInput(self->view.window); + this->completions = release(this->completions); + if (this->delegate.didEndEditing) { this->delegate.didEndEditing(this); } @@ -453,6 +567,8 @@ static void setAttributedText(TextView *self, const char *attributedText) { self->position = self->attributedText->length; + self->completions = release(self->completions); + $((View *) self, setNeedsLayout); } } diff --git a/Sources/ObjectivelyMVC/TextView.h b/Sources/ObjectivelyMVC/TextView.h index 29c7c0cf..4371af4c 100644 --- a/Sources/ObjectivelyMVC/TextView.h +++ b/Sources/ObjectivelyMVC/TextView.h @@ -23,6 +23,7 @@ #pragma once +#include #include #include @@ -69,6 +70,19 @@ struct TextViewDelegate { * @param textView The TextView. */ void (*didEndEditing)(TextView *textView); + + /** + * @brief Delegate callback for auto-completing the text before the cursor with Tab. + * @param textView The TextView. + * @param prefix The text before the cursor. + * @return The completions for `prefix`, in the order that Tab cycles through them, or `NULL`. + * @remarks Each completion SHOULD begin with `prefix`, as it replaces `prefix`. The TextView + * completes to the longest prefix the completions share, and then Tab and Shift+Tab cycle + * through them. The TextView releases the returned Array. If this is `NULL`, or returns + * `NULL`, an empty Array, or only `prefix` itself, Tab ends editing and advances, as it does + * for any other Control. + */ + Array *(*completionsForPrefix)(TextView *textView, const char *prefix); }; /** @@ -119,6 +133,19 @@ struct TextView { * @brief The text. */ Text *text; + + /** + * @brief The completions that Tab cycles through, or `NULL`. + * @private + */ + Array *completions; + + /** + * @brief The index of the completion before the cursor, or the count of `completions` when + * the text before the cursor is their shared prefix. + * @private + */ + size_t completion; }; /** diff --git a/Sources/ObjectivelyMVC/WindowController.c b/Sources/ObjectivelyMVC/WindowController.c index 8ca8e582..7b80d8d6 100644 --- a/Sources/ObjectivelyMVC/WindowController.c +++ b/Sources/ObjectivelyMVC/WindowController.c @@ -26,6 +26,7 @@ #include #include "Log.h" +#include "TextView.h" #include "WindowController.h" #define _Class _WindowController @@ -352,14 +353,24 @@ static void respondToEvent(WindowController *self, const SDL_Event *event) { if (event->type == SDL_EVENT_KEY_DOWN) { if (event->key.key == SDLK_TAB) { - if (event->key.mod & SDL_KMOD_SHIFT) { - keyResponder = $(self, previousKeyResponder, keyResponder); - } else { - keyResponder = $(self, nextKeyResponder, keyResponder); - } + View *current = $(self, keyResponder); - if (keyResponder) { - $(keyResponder, becomeKeyResponder); + // An editing TextView that is still the key responder kept Tab to auto-complete + const bool consumed = current && current == keyResponder && + $((Object *) current, isKindOfClass, _TextView()) && + ((TextView *) current)->isEditable && + !$((Control *) current, isDisabled); + + if (!consumed) { + if (event->key.mod & SDL_KMOD_SHIFT) { + keyResponder = $(self, previousKeyResponder, keyResponder); + } else { + keyResponder = $(self, nextKeyResponder, keyResponder); + } + + if (keyResponder) { + $(keyResponder, becomeKeyResponder); + } } } From b362d2767a7e5252d644dbb49c38297e288a8fcf Mon Sep 17 00:00:00 2001 From: Jay Dolan Date: Fri, 25 Sep 2026 10:17:32 -0400 Subject: [PATCH 06/11] Fix Gentoo compilation error. --- Sources/ObjectivelyMVC/Image.c | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/ObjectivelyMVC/Image.c b/Sources/ObjectivelyMVC/Image.c index e0ee751b..8bffc92d 100644 --- a/Sources/ObjectivelyMVC/Image.c +++ b/Sources/ObjectivelyMVC/Image.c @@ -17,6 +17,7 @@ #include #include +#include #include From 27177e22c5f1a5a009a6d9b58ba64cb2dfcec95c Mon Sep 17 00:00:00 2001 From: Jay Dolan Date: Fri, 25 Sep 2026 10:18:39 -0400 Subject: [PATCH 07/11] Add optional TextView tab completion with TextViewDelegate::completionsForPrefix. --- Assets/stylesheet.css.h | 189 ++++++++++---------- Tests/ObjectivelyMVC/.gitignore | 1 + Tests/ObjectivelyMVC/Makefile.am | 1 + Tests/ObjectivelyMVC/TextView.c | 295 +++++++++++++++++++++++++++++++ 4 files changed, 394 insertions(+), 92 deletions(-) create mode 100644 Tests/ObjectivelyMVC/TextView.c diff --git a/Assets/stylesheet.css.h b/Assets/stylesheet.css.h index b9de1c9f..3ab756c1 100644 --- a/Assets/stylesheet.css.h +++ b/Assets/stylesheet.css.h @@ -499,100 +499,105 @@ unsigned char stylesheet_css[] = { 0x69, 0x64, 0x74, 0x68, 0x3b, 0x0a, 0x7d, 0x0a, 0x0a, 0x54, 0x61, 0x62, 0x56, 0x69, 0x65, 0x77, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x73, 0x70, 0x61, 0x63, 0x69, 0x6e, 0x67, 0x3a, 0x20, 0x34, 0x3b, 0x0a, 0x7d, 0x0a, 0x0a, - 0x54, 0x61, 0x62, 0x56, 0x69, 0x65, 0x77, 0x20, 0x3e, 0x20, 0x2e, 0x74, - 0x61, 0x62, 0x50, 0x61, 0x67, 0x65, 0x56, 0x69, 0x65, 0x77, 0x20, 0x7b, - 0x0a, 0x20, 0x20, 0x62, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x2d, 0x63, 0x6f, - 0x6c, 0x6f, 0x72, 0x3a, 0x20, 0x73, 0x69, 0x6c, 0x76, 0x65, 0x72, 0x3b, - 0x0a, 0x20, 0x20, 0x62, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x2d, 0x72, 0x61, - 0x64, 0x69, 0x75, 0x73, 0x3a, 0x20, 0x34, 0x3b, 0x0a, 0x20, 0x20, 0x62, - 0x6f, 0x72, 0x64, 0x65, 0x72, 0x2d, 0x77, 0x69, 0x64, 0x74, 0x68, 0x3a, - 0x20, 0x31, 0x3b, 0x0a, 0x7d, 0x0a, 0x0a, 0x54, 0x61, 0x62, 0x56, 0x69, - 0x65, 0x77, 0x20, 0x3e, 0x20, 0x2e, 0x74, 0x61, 0x62, 0x53, 0x65, 0x6c, - 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x56, 0x69, 0x65, 0x77, 0x20, 0x7b, - 0x0a, 0x20, 0x20, 0x61, 0x75, 0x74, 0x6f, 0x72, 0x65, 0x73, 0x69, 0x7a, - 0x69, 0x6e, 0x67, 0x2d, 0x6d, 0x61, 0x73, 0x6b, 0x3a, 0x20, 0x63, 0x6f, - 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x20, 0x7c, 0x20, 0x77, 0x69, 0x64, 0x74, - 0x68, 0x3b, 0x0a, 0x20, 0x20, 0x61, 0x78, 0x69, 0x73, 0x3a, 0x20, 0x68, - 0x6f, 0x72, 0x69, 0x7a, 0x6f, 0x6e, 0x74, 0x61, 0x6c, 0x3b, 0x0a, 0x20, - 0x20, 0x64, 0x69, 0x73, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, - 0x6e, 0x3a, 0x20, 0x66, 0x69, 0x6c, 0x6c, 0x2d, 0x65, 0x71, 0x75, 0x61, - 0x6c, 0x6c, 0x79, 0x3b, 0x0a, 0x20, 0x20, 0x73, 0x70, 0x61, 0x63, 0x69, - 0x6e, 0x67, 0x3a, 0x20, 0x34, 0x3b, 0x0a, 0x7d, 0x0a, 0x0a, 0x54, 0x61, - 0x62, 0x56, 0x69, 0x65, 0x77, 0x20, 0x3e, 0x20, 0x2e, 0x74, 0x61, 0x62, - 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x56, 0x69, 0x65, - 0x77, 0x20, 0x3e, 0x20, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x20, 0x7b, 0x0a, - 0x20, 0x20, 0x70, 0x61, 0x64, 0x64, 0x69, 0x6e, 0x67, 0x3a, 0x20, 0x32, - 0x20, 0x32, 0x20, 0x32, 0x20, 0x32, 0x3b, 0x0a, 0x7d, 0x0a, 0x0a, 0x54, - 0x61, 0x62, 0x56, 0x69, 0x65, 0x77, 0x20, 0x3e, 0x20, 0x2e, 0x74, 0x61, - 0x62, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x56, 0x69, - 0x65, 0x77, 0x20, 0x3e, 0x20, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x2e, 0x73, - 0x65, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x20, 0x7b, 0x0a, 0x20, 0x20, - 0x62, 0x61, 0x63, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x2d, 0x63, - 0x6f, 0x6c, 0x6f, 0x72, 0x3a, 0x20, 0x64, 0x61, 0x72, 0x6b, 0x67, 0x72, - 0x61, 0x79, 0x3b, 0x0a, 0x7d, 0x0a, 0x0a, 0x54, 0x61, 0x62, 0x56, 0x69, - 0x65, 0x77, 0x20, 0x3e, 0x20, 0x2e, 0x74, 0x61, 0x62, 0x53, 0x65, 0x6c, - 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x56, 0x69, 0x65, 0x77, 0x20, 0x3e, - 0x20, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x2e, 0x64, 0x69, 0x73, 0x61, 0x62, - 0x6c, 0x65, 0x64, 0x20, 0x3e, 0x20, 0x54, 0x65, 0x78, 0x74, 0x20, 0x7b, - 0x0a, 0x20, 0x20, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x3a, 0x20, 0x67, 0x72, - 0x65, 0x79, 0x3b, 0x0a, 0x7d, 0x0a, 0x0a, 0x54, 0x61, 0x62, 0x56, 0x69, - 0x65, 0x77, 0x20, 0x3e, 0x20, 0x2e, 0x74, 0x61, 0x62, 0x53, 0x65, 0x6c, - 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x56, 0x69, 0x65, 0x77, 0x20, 0x3e, - 0x20, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x20, 0x3e, 0x20, 0x54, 0x65, 0x78, - 0x74, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x61, 0x6c, 0x69, 0x67, 0x6e, 0x6d, - 0x65, 0x6e, 0x74, 0x3a, 0x20, 0x6d, 0x69, 0x64, 0x64, 0x6c, 0x65, 0x2d, - 0x63, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x3b, 0x0a, 0x7d, 0x0a, 0x0a, 0x54, - 0x65, 0x78, 0x74, 0x56, 0x69, 0x65, 0x77, 0x20, 0x7b, 0x0a, 0x20, 0x20, - 0x61, 0x75, 0x74, 0x6f, 0x72, 0x65, 0x73, 0x69, 0x7a, 0x69, 0x6e, 0x67, - 0x2d, 0x6d, 0x61, 0x73, 0x6b, 0x3a, 0x20, 0x63, 0x6f, 0x6e, 0x74, 0x61, - 0x69, 0x6e, 0x3b, 0x0a, 0x20, 0x20, 0x62, 0x61, 0x63, 0x6b, 0x67, 0x72, - 0x6f, 0x75, 0x6e, 0x64, 0x2d, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x3a, 0x20, - 0x23, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x36, 0x36, 0x3b, 0x0a, 0x20, - 0x20, 0x62, 0x65, 0x76, 0x65, 0x6c, 0x3a, 0x20, 0x69, 0x6e, 0x73, 0x65, - 0x74, 0x3b, 0x0a, 0x20, 0x20, 0x63, 0x6c, 0x69, 0x70, 0x73, 0x2d, 0x73, - 0x75, 0x62, 0x76, 0x69, 0x65, 0x77, 0x73, 0x3a, 0x20, 0x74, 0x72, 0x75, - 0x65, 0x3b, 0x0a, 0x20, 0x20, 0x65, 0x64, 0x69, 0x74, 0x61, 0x62, 0x6c, - 0x65, 0x3a, 0x20, 0x74, 0x72, 0x75, 0x65, 0x3b, 0x0a, 0x20, 0x20, 0x6d, - 0x69, 0x6e, 0x2d, 0x77, 0x69, 0x64, 0x74, 0x68, 0x3a, 0x20, 0x31, 0x36, - 0x30, 0x3b, 0x0a, 0x20, 0x20, 0x70, 0x61, 0x64, 0x64, 0x69, 0x6e, 0x67, - 0x3a, 0x20, 0x34, 0x20, 0x34, 0x20, 0x34, 0x20, 0x34, 0x3b, 0x0a, 0x7d, - 0x0a, 0x0a, 0x54, 0x65, 0x78, 0x74, 0x56, 0x69, 0x65, 0x77, 0x20, 0x3e, - 0x20, 0x54, 0x65, 0x78, 0x74, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x61, 0x6c, - 0x69, 0x67, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x3a, 0x20, 0x6d, 0x69, 0x64, - 0x64, 0x6c, 0x65, 0x2d, 0x6c, 0x65, 0x66, 0x74, 0x3b, 0x0a, 0x7d, 0x0a, - 0x0a, 0x54, 0x65, 0x78, 0x74, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x63, 0x6f, - 0x6c, 0x6f, 0x72, 0x3a, 0x20, 0x67, 0x68, 0x6f, 0x73, 0x74, 0x77, 0x68, - 0x69, 0x74, 0x65, 0x3b, 0x0a, 0x7d, 0x0a, 0x0a, 0x2e, 0x63, 0x6f, 0x6e, - 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x61, + 0x2e, 0x74, 0x61, 0x62, 0x56, 0x69, 0x65, 0x77, 0x43, 0x6f, 0x6e, 0x74, + 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x61, 0x75, 0x74, 0x6f, 0x72, 0x65, 0x73, 0x69, 0x7a, 0x69, 0x6e, 0x67, 0x2d, 0x6d, 0x61, 0x73, 0x6b, 0x3a, 0x20, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, - 0x6e, 0x3b, 0x0a, 0x7d, 0x0a, 0x0a, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6f, - 0x6e, 0x73, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x61, 0x6c, 0x69, 0x67, 0x6e, - 0x6d, 0x65, 0x6e, 0x74, 0x3a, 0x20, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, - 0x61, 0x6c, 0x3b, 0x0a, 0x20, 0x20, 0x62, 0x61, 0x63, 0x6b, 0x67, 0x72, - 0x6f, 0x75, 0x6e, 0x64, 0x2d, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x3a, 0x20, - 0x63, 0x68, 0x61, 0x72, 0x63, 0x6f, 0x61, 0x6c, 0x3b, 0x0a, 0x20, 0x20, - 0x62, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x2d, 0x63, 0x6f, 0x6c, 0x6f, 0x72, - 0x3a, 0x20, 0x23, 0x64, 0x65, 0x64, 0x65, 0x64, 0x65, 0x61, 0x61, 0x3b, - 0x0a, 0x20, 0x20, 0x62, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x2d, 0x72, 0x61, - 0x64, 0x69, 0x75, 0x73, 0x3a, 0x20, 0x34, 0x3b, 0x0a, 0x20, 0x20, 0x62, - 0x6f, 0x72, 0x64, 0x65, 0x72, 0x2d, 0x77, 0x69, 0x64, 0x74, 0x68, 0x3a, - 0x20, 0x31, 0x3b, 0x0a, 0x20, 0x20, 0x70, 0x61, 0x64, 0x64, 0x69, 0x6e, - 0x67, 0x3a, 0x20, 0x32, 0x3b, 0x0a, 0x7d, 0x0a, 0x0a, 0x2e, 0x6f, 0x70, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x20, 0x3e, 0x20, 0x4f, 0x70, 0x74, 0x69, - 0x6f, 0x6e, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x62, 0x6f, 0x72, 0x64, 0x65, - 0x72, 0x2d, 0x72, 0x61, 0x64, 0x69, 0x75, 0x73, 0x3a, 0x20, 0x32, 0x3b, - 0x0a, 0x20, 0x20, 0x70, 0x61, 0x64, 0x64, 0x69, 0x6e, 0x67, 0x3a, 0x20, - 0x32, 0x20, 0x34, 0x20, 0x32, 0x20, 0x34, 0x3b, 0x0a, 0x7d, 0x0a, 0x0a, - 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x20, 0x3e, 0x20, 0x4f, - 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x3a, 0x68, 0x6f, 0x76, 0x65, 0x72, 0x20, - 0x7b, 0x0a, 0x20, 0x20, 0x62, 0x61, 0x63, 0x6b, 0x67, 0x72, 0x6f, 0x75, - 0x6e, 0x64, 0x2d, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x3a, 0x20, 0x64, 0x61, - 0x72, 0x6b, 0x67, 0x72, 0x61, 0x79, 0x3b, 0x0a, 0x7d, 0x0a, 0x0a, 0x2e, - 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x20, 0x3e, 0x20, 0x4f, 0x70, - 0x74, 0x69, 0x6f, 0x6e, 0x3a, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x65, + 0x6e, 0x3b, 0x0a, 0x7d, 0x0a, 0x0a, 0x54, 0x61, 0x62, 0x56, 0x69, 0x65, + 0x77, 0x20, 0x3e, 0x20, 0x2e, 0x74, 0x61, 0x62, 0x50, 0x61, 0x67, 0x65, + 0x56, 0x69, 0x65, 0x77, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x62, 0x6f, 0x72, + 0x64, 0x65, 0x72, 0x2d, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x3a, 0x20, 0x73, + 0x69, 0x6c, 0x76, 0x65, 0x72, 0x3b, 0x0a, 0x20, 0x20, 0x62, 0x6f, 0x72, + 0x64, 0x65, 0x72, 0x2d, 0x72, 0x61, 0x64, 0x69, 0x75, 0x73, 0x3a, 0x20, + 0x34, 0x3b, 0x0a, 0x20, 0x20, 0x62, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x2d, + 0x77, 0x69, 0x64, 0x74, 0x68, 0x3a, 0x20, 0x31, 0x3b, 0x0a, 0x7d, 0x0a, + 0x0a, 0x54, 0x61, 0x62, 0x56, 0x69, 0x65, 0x77, 0x20, 0x3e, 0x20, 0x2e, + 0x74, 0x61, 0x62, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x56, 0x69, 0x65, 0x77, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x61, 0x75, 0x74, + 0x6f, 0x72, 0x65, 0x73, 0x69, 0x7a, 0x69, 0x6e, 0x67, 0x2d, 0x6d, 0x61, + 0x73, 0x6b, 0x3a, 0x20, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x20, + 0x7c, 0x20, 0x77, 0x69, 0x64, 0x74, 0x68, 0x3b, 0x0a, 0x20, 0x20, 0x61, + 0x78, 0x69, 0x73, 0x3a, 0x20, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x6f, 0x6e, + 0x74, 0x61, 0x6c, 0x3b, 0x0a, 0x20, 0x20, 0x64, 0x69, 0x73, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x3a, 0x20, 0x66, 0x69, 0x6c, + 0x6c, 0x2d, 0x65, 0x71, 0x75, 0x61, 0x6c, 0x6c, 0x79, 0x3b, 0x0a, 0x20, + 0x20, 0x73, 0x70, 0x61, 0x63, 0x69, 0x6e, 0x67, 0x3a, 0x20, 0x34, 0x3b, + 0x0a, 0x7d, 0x0a, 0x0a, 0x54, 0x61, 0x62, 0x56, 0x69, 0x65, 0x77, 0x20, + 0x3e, 0x20, 0x2e, 0x74, 0x61, 0x62, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x56, 0x69, 0x65, 0x77, 0x20, 0x3e, 0x20, 0x4c, 0x61, + 0x62, 0x65, 0x6c, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x70, 0x61, 0x64, 0x64, + 0x69, 0x6e, 0x67, 0x3a, 0x20, 0x32, 0x20, 0x32, 0x20, 0x32, 0x20, 0x32, + 0x3b, 0x0a, 0x7d, 0x0a, 0x0a, 0x54, 0x61, 0x62, 0x56, 0x69, 0x65, 0x77, + 0x20, 0x3e, 0x20, 0x2e, 0x74, 0x61, 0x62, 0x53, 0x65, 0x6c, 0x65, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x56, 0x69, 0x65, 0x77, 0x20, 0x3e, 0x20, 0x4c, + 0x61, 0x62, 0x65, 0x6c, 0x2e, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x62, 0x61, 0x63, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x2d, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x3a, 0x20, - 0x64, 0x69, 0x6d, 0x67, 0x72, 0x61, 0x79, 0x3b, 0x0a, 0x7d, 0x0a, 0x00 + 0x64, 0x61, 0x72, 0x6b, 0x67, 0x72, 0x61, 0x79, 0x3b, 0x0a, 0x7d, 0x0a, + 0x0a, 0x54, 0x61, 0x62, 0x56, 0x69, 0x65, 0x77, 0x20, 0x3e, 0x20, 0x2e, + 0x74, 0x61, 0x62, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x56, 0x69, 0x65, 0x77, 0x20, 0x3e, 0x20, 0x4c, 0x61, 0x62, 0x65, 0x6c, + 0x2e, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x20, 0x3e, 0x20, + 0x54, 0x65, 0x78, 0x74, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x63, 0x6f, 0x6c, + 0x6f, 0x72, 0x3a, 0x20, 0x67, 0x72, 0x65, 0x79, 0x3b, 0x0a, 0x7d, 0x0a, + 0x0a, 0x54, 0x61, 0x62, 0x56, 0x69, 0x65, 0x77, 0x20, 0x3e, 0x20, 0x2e, + 0x74, 0x61, 0x62, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x56, 0x69, 0x65, 0x77, 0x20, 0x3e, 0x20, 0x4c, 0x61, 0x62, 0x65, 0x6c, + 0x20, 0x3e, 0x20, 0x54, 0x65, 0x78, 0x74, 0x20, 0x7b, 0x0a, 0x20, 0x20, + 0x61, 0x6c, 0x69, 0x67, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x3a, 0x20, 0x6d, + 0x69, 0x64, 0x64, 0x6c, 0x65, 0x2d, 0x63, 0x65, 0x6e, 0x74, 0x65, 0x72, + 0x3b, 0x0a, 0x7d, 0x0a, 0x0a, 0x54, 0x65, 0x78, 0x74, 0x56, 0x69, 0x65, + 0x77, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x61, 0x75, 0x74, 0x6f, 0x72, 0x65, + 0x73, 0x69, 0x7a, 0x69, 0x6e, 0x67, 0x2d, 0x6d, 0x61, 0x73, 0x6b, 0x3a, + 0x20, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x3b, 0x0a, 0x20, 0x20, + 0x62, 0x61, 0x63, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x2d, 0x63, + 0x6f, 0x6c, 0x6f, 0x72, 0x3a, 0x20, 0x23, 0x32, 0x32, 0x32, 0x32, 0x32, + 0x32, 0x36, 0x36, 0x3b, 0x0a, 0x20, 0x20, 0x62, 0x65, 0x76, 0x65, 0x6c, + 0x3a, 0x20, 0x69, 0x6e, 0x73, 0x65, 0x74, 0x3b, 0x0a, 0x20, 0x20, 0x63, + 0x6c, 0x69, 0x70, 0x73, 0x2d, 0x73, 0x75, 0x62, 0x76, 0x69, 0x65, 0x77, + 0x73, 0x3a, 0x20, 0x74, 0x72, 0x75, 0x65, 0x3b, 0x0a, 0x20, 0x20, 0x65, + 0x64, 0x69, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x3a, 0x20, 0x74, 0x72, 0x75, + 0x65, 0x3b, 0x0a, 0x20, 0x20, 0x6d, 0x69, 0x6e, 0x2d, 0x77, 0x69, 0x64, + 0x74, 0x68, 0x3a, 0x20, 0x31, 0x36, 0x30, 0x3b, 0x0a, 0x20, 0x20, 0x70, + 0x61, 0x64, 0x64, 0x69, 0x6e, 0x67, 0x3a, 0x20, 0x34, 0x20, 0x34, 0x20, + 0x34, 0x20, 0x34, 0x3b, 0x0a, 0x7d, 0x0a, 0x0a, 0x54, 0x65, 0x78, 0x74, + 0x56, 0x69, 0x65, 0x77, 0x20, 0x3e, 0x20, 0x54, 0x65, 0x78, 0x74, 0x20, + 0x7b, 0x0a, 0x20, 0x20, 0x61, 0x6c, 0x69, 0x67, 0x6e, 0x6d, 0x65, 0x6e, + 0x74, 0x3a, 0x20, 0x6d, 0x69, 0x64, 0x64, 0x6c, 0x65, 0x2d, 0x6c, 0x65, + 0x66, 0x74, 0x3b, 0x0a, 0x7d, 0x0a, 0x0a, 0x54, 0x65, 0x78, 0x74, 0x20, + 0x7b, 0x0a, 0x20, 0x20, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x3a, 0x20, 0x67, + 0x68, 0x6f, 0x73, 0x74, 0x77, 0x68, 0x69, 0x74, 0x65, 0x3b, 0x0a, 0x7d, + 0x0a, 0x0a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, + 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x61, 0x75, 0x74, 0x6f, 0x72, 0x65, 0x73, + 0x69, 0x7a, 0x69, 0x6e, 0x67, 0x2d, 0x6d, 0x61, 0x73, 0x6b, 0x3a, 0x20, + 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x3b, 0x0a, 0x7d, 0x0a, 0x0a, + 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x20, 0x7b, 0x0a, 0x20, + 0x20, 0x61, 0x6c, 0x69, 0x67, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x3a, 0x20, + 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x3b, 0x0a, 0x20, 0x20, + 0x62, 0x61, 0x63, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x2d, 0x63, + 0x6f, 0x6c, 0x6f, 0x72, 0x3a, 0x20, 0x63, 0x68, 0x61, 0x72, 0x63, 0x6f, + 0x61, 0x6c, 0x3b, 0x0a, 0x20, 0x20, 0x62, 0x6f, 0x72, 0x64, 0x65, 0x72, + 0x2d, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x3a, 0x20, 0x23, 0x64, 0x65, 0x64, + 0x65, 0x64, 0x65, 0x61, 0x61, 0x3b, 0x0a, 0x20, 0x20, 0x62, 0x6f, 0x72, + 0x64, 0x65, 0x72, 0x2d, 0x72, 0x61, 0x64, 0x69, 0x75, 0x73, 0x3a, 0x20, + 0x34, 0x3b, 0x0a, 0x20, 0x20, 0x62, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x2d, + 0x77, 0x69, 0x64, 0x74, 0x68, 0x3a, 0x20, 0x31, 0x3b, 0x0a, 0x20, 0x20, + 0x70, 0x61, 0x64, 0x64, 0x69, 0x6e, 0x67, 0x3a, 0x20, 0x32, 0x3b, 0x0a, + 0x7d, 0x0a, 0x0a, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x20, + 0x3e, 0x20, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x7b, 0x0a, 0x20, + 0x20, 0x62, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x2d, 0x72, 0x61, 0x64, 0x69, + 0x75, 0x73, 0x3a, 0x20, 0x32, 0x3b, 0x0a, 0x20, 0x20, 0x70, 0x61, 0x64, + 0x64, 0x69, 0x6e, 0x67, 0x3a, 0x20, 0x32, 0x20, 0x34, 0x20, 0x32, 0x20, + 0x34, 0x3b, 0x0a, 0x7d, 0x0a, 0x0a, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x20, 0x3e, 0x20, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x3a, + 0x68, 0x6f, 0x76, 0x65, 0x72, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x62, 0x61, + 0x63, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x2d, 0x63, 0x6f, 0x6c, + 0x6f, 0x72, 0x3a, 0x20, 0x64, 0x61, 0x72, 0x6b, 0x67, 0x72, 0x61, 0x79, + 0x3b, 0x0a, 0x7d, 0x0a, 0x0a, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x20, 0x3e, 0x20, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x3a, 0x73, + 0x65, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x20, 0x7b, 0x0a, 0x20, 0x20, + 0x62, 0x61, 0x63, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x2d, 0x63, + 0x6f, 0x6c, 0x6f, 0x72, 0x3a, 0x20, 0x64, 0x69, 0x6d, 0x67, 0x72, 0x61, + 0x79, 0x3b, 0x0a, 0x7d, 0x0a, 0x00 }; -unsigned int stylesheet_css_len = 7139; +unsigned int stylesheet_css_len = 7193; diff --git a/Tests/ObjectivelyMVC/.gitignore b/Tests/ObjectivelyMVC/.gitignore index bc0e68c8..76497708 100644 --- a/Tests/ObjectivelyMVC/.gitignore +++ b/Tests/ObjectivelyMVC/.gitignore @@ -8,4 +8,5 @@ Selector Style Stylesheet Text +TextView View diff --git a/Tests/ObjectivelyMVC/Makefile.am b/Tests/ObjectivelyMVC/Makefile.am index 64f5b349..91447c43 100644 --- a/Tests/ObjectivelyMVC/Makefile.am +++ b/Tests/ObjectivelyMVC/Makefile.am @@ -13,6 +13,7 @@ TESTS = \ Style \ Stylesheet \ Text \ + TextView \ View CFLAGS += \ diff --git a/Tests/ObjectivelyMVC/TextView.c b/Tests/ObjectivelyMVC/TextView.c new file mode 100644 index 00000000..7fe7608c --- /dev/null +++ b/Tests/ObjectivelyMVC/TextView.c @@ -0,0 +1,295 @@ +/* + * ObjectivelyMVC: Object oriented MVC framework for SDL3 and C. + * Copyright (C) 2014 Jay Dolan + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + */ + +#include +#include +#include + +#include "ObjectivelyMVC.h" + +static const char **candidates; +static int requests; +static char lastPrefix[64]; + +/** + * @brief TextViewDelegate callback answering the candidates that begin with `prefix`. + */ +static Array *completionsForPrefix(TextView *textView, const char *prefix) { + + requests++; + strncpy(lastPrefix, prefix, sizeof(lastPrefix) - 1); + + Array *completions = $$(Array, array); + + for (const char **c = candidates; *c; c++) { + if (!strncmp(*c, prefix, strlen(prefix))) { + String *completion = $$(String, stringWithCharacters, *c); + $(completions, addObject, completion); + release(completion); + } + } + + return completions; +} + +static TextView *textViewWithText(const char *text) { + + TextView *textView = $(alloc(TextView), initWithFrame, NULL); + textView->delegate.completionsForPrefix = completionsForPrefix; + + $(textView, setAttributedText, text); + + requests = 0; + memset(lastPrefix, 0, sizeof(lastPrefix)); + + return textView; +} + +static void keyDown(TextView *textView, SDL_Keycode key, SDL_Keymod mod) { + + SDL_Event event = { .type = SDL_EVENT_KEY_DOWN }; + event.key.key = key; + event.key.mod = mod; + + $((Control *) textView, captureEvent, &event); +} + +static const char *text(const TextView *textView) { + return textView->attributedText->chars ?: ""; +} + +START_TEST(tabCyclesWhenSharedPrefixDoesNotExtend) { + + candidates = (const char *[]) { "abc", "abd", "abe", "x", NULL }; + + TextView *textView = textViewWithText("ab"); + + keyDown(textView, SDLK_TAB, 0); + ck_assert_str_eq("abc", text(textView)); + + keyDown(textView, SDLK_TAB, 0); + ck_assert_str_eq("abd", text(textView)); + + keyDown(textView, SDLK_TAB, 0); + ck_assert_str_eq("abe", text(textView)); + + keyDown(textView, SDLK_TAB, 0); + ck_assert_str_eq("abc", text(textView)); + + keyDown(textView, SDLK_TAB, SDL_KMOD_SHIFT); + ck_assert_str_eq("abe", text(textView)); + + ck_assert_int_eq(1, requests); + + release(textView); + +} END_TEST + +START_TEST(tabCompletesSharedPrefixThenCycles) { + + candidates = (const char *[]) { "abc", "abd", NULL }; + + TextView *textView = textViewWithText("a"); + + keyDown(textView, SDLK_TAB, 0); + ck_assert_str_eq("ab", text(textView)); + + keyDown(textView, SDLK_TAB, 0); + ck_assert_str_eq("abc", text(textView)); + + keyDown(textView, SDLK_TAB, 0); + ck_assert_str_eq("abd", text(textView)); + + release(textView); + + textView = textViewWithText("a"); + + keyDown(textView, SDLK_TAB, 0); + ck_assert_str_eq("ab", text(textView)); + + keyDown(textView, SDLK_TAB, SDL_KMOD_SHIFT); + ck_assert_str_eq("abd", text(textView)); + + release(textView); + + textView = textViewWithText("ab"); + + keyDown(textView, SDLK_TAB, SDL_KMOD_SHIFT); + ck_assert_str_eq("abd", text(textView)); + + release(textView); + +} END_TEST + +START_TEST(tabCompletesBeforeCursor) { + + candidates = (const char *[]) { "abc", NULL }; + + TextView *textView = textViewWithText("a xyz"); + textView->position = 1; + + keyDown(textView, SDLK_TAB, 0); + ck_assert_str_eq("a", lastPrefix); + ck_assert_str_eq("abc xyz", text(textView)); + ck_assert_int_eq(3, textView->position); + + release(textView); + +} END_TEST + +START_TEST(tabAfterLoneCompletionRequestsAgain) { + + candidates = (const char *[]) { "dir/", NULL }; + + TextView *textView = textViewWithText("d"); + + keyDown(textView, SDLK_TAB, 0); + ck_assert_str_eq("dir/", text(textView)); + + candidates = (const char *[]) { "dir/x", "dir/y", NULL }; + + keyDown(textView, SDLK_TAB, 0); + ck_assert_str_eq("dir/", lastPrefix); + ck_assert_str_eq("dir/x", text(textView)); + ck_assert_int_eq(2, requests); + + release(textView); + +} END_TEST + +START_TEST(tabWithNothingToCompleteChangesNothing) { + + candidates = (const char *[]) { "abc", "x", NULL }; + + TextView *textView = textViewWithText("abc"); + + keyDown(textView, SDLK_TAB, 0); + ck_assert_str_eq("abc", text(textView)); + ck_assert_ptr_null(textView->completions); + + $(textView, setAttributedText, "q"); + + keyDown(textView, SDLK_TAB, 0); + ck_assert_str_eq("q", text(textView)); + + textView->delegate.completionsForPrefix = NULL; + + keyDown(textView, SDLK_TAB, 0); + ck_assert_str_eq("q", text(textView)); + ck_assert_int_eq(2, requests); + + release(textView); + +} END_TEST + +START_TEST(editingEndsCycling) { + + candidates = (const char *[]) { "abc", "abd", NULL }; + + TextView *textView = textViewWithText("ab"); + + keyDown(textView, SDLK_TAB, 0); + ck_assert_str_eq("abc", text(textView)); + + // Pressing Shift on its own must not end cycling, or Shift+Tab could never cycle backward + keyDown(textView, SDLK_LSHIFT, 0); + keyDown(textView, SDLK_TAB, SDL_KMOD_SHIFT); + ck_assert_str_eq("abd", text(textView)); + ck_assert_int_eq(1, requests); + + keyDown(textView, SDLK_BACKSPACE, 0); + ck_assert_str_eq("ab", text(textView)); + ck_assert_ptr_null(textView->completions); + + keyDown(textView, SDLK_TAB, 0); + ck_assert_str_eq("abc", text(textView)); + ck_assert_int_eq(2, requests); + + keyDown(textView, SDLK_LEFT, 0); + ck_assert_ptr_null(textView->completions); + + release(textView); + +} END_TEST + +START_TEST(sharedPrefixKeepsWholeCharacters) { + + candidates = (const char *[]) { "a\xc3\xa9", "a\xc3\xa8", NULL }; + + TextView *textView = textViewWithText(""); + + keyDown(textView, SDLK_TAB, 0); + ck_assert_str_eq("a", text(textView)); + + release(textView); + + candidates = (const char *[]) { "a^1x", "a^2x", NULL }; + + textView = textViewWithText(""); + + keyDown(textView, SDLK_TAB, 0); + ck_assert_str_eq("a", text(textView)); + + release(textView); + +} END_TEST + +START_TEST(cyclingToEmptyCompletionClearsText) { + + candidates = (const char *[]) { "", "abc", NULL }; + + TextView *textView = textViewWithText(""); + + keyDown(textView, SDLK_TAB, 0); + ck_assert_str_eq("", text(textView)); + + keyDown(textView, SDLK_TAB, 0); + ck_assert_str_eq("abc", text(textView)); + + keyDown(textView, SDLK_TAB, 0); + ck_assert_str_eq("", text(textView)); + ck_assert_int_eq(0, textView->attributedText->length); + ck_assert_int_eq(0, textView->position); + + release(textView); + +} END_TEST + +int main(int argc, char **argv) { + + TCase *tcase = tcase_create("TextView"); + tcase_add_test(tcase, tabCyclesWhenSharedPrefixDoesNotExtend); + tcase_add_test(tcase, tabCompletesSharedPrefixThenCycles); + tcase_add_test(tcase, tabCompletesBeforeCursor); + tcase_add_test(tcase, tabAfterLoneCompletionRequestsAgain); + tcase_add_test(tcase, tabWithNothingToCompleteChangesNothing); + tcase_add_test(tcase, editingEndsCycling); + tcase_add_test(tcase, sharedPrefixKeepsWholeCharacters); + tcase_add_test(tcase, cyclingToEmptyCompletionClearsText); + + Suite *suite = suite_create("TextView"); + suite_add_tcase(suite, tcase); + + SRunner *runner = srunner_create(suite); + + srunner_run_all(runner, CK_VERBOSE); + int failed = srunner_ntests_failed(runner); + + srunner_free(runner); + + return failed; +} From b349932cab2a9ce5e6a53b8d26e846b9c863462a Mon Sep 17 00:00:00 2001 From: Jay Dolan Date: Fri, 25 Sep 2026 10:18:49 -0400 Subject: [PATCH 08/11] Add new tests to Xcode. --- ObjectivelyMVC.xcodeproj/project.pbxproj | 347 +++++++++++++++++- .../xcschemes/ObjectivelyMVC-Image.xcscheme | 80 ++++ .../xcschemes/ObjectivelyMVC-Text.xcscheme | 80 ++++ .../ObjectivelyMVC-TextView.xcscheme | 80 ++++ 4 files changed, 586 insertions(+), 1 deletion(-) create mode 100644 ObjectivelyMVC.xcodeproj/xcshareddata/xcschemes/ObjectivelyMVC-Image.xcscheme create mode 100644 ObjectivelyMVC.xcodeproj/xcshareddata/xcschemes/ObjectivelyMVC-Text.xcscheme create mode 100644 ObjectivelyMVC.xcodeproj/xcshareddata/xcschemes/ObjectivelyMVC-TextView.xcscheme diff --git a/ObjectivelyMVC.xcodeproj/project.pbxproj b/ObjectivelyMVC.xcodeproj/project.pbxproj index 59932995..423b9753 100644 --- a/ObjectivelyMVC.xcodeproj/project.pbxproj +++ b/ObjectivelyMVC.xcodeproj/project.pbxproj @@ -28,6 +28,9 @@ dependencies = ( CE0C2218304CC1FD008A6A2A /* PBXTargetDependency */, CED40A98304B2DFA0090C57A /* PBXTargetDependency */, + B3DD5991CDBD4582092CC4F6 /* PBXTargetDependency */, + 00E123F744F252F431ACB5AB /* PBXTargetDependency */, + 0850FEAA7E79310A5E7C5ED5 /* PBXTargetDependency */, CED40A633046145B0090C57A /* PBXTargetDependency */, CE34C8A51FB2A0210025F231 /* PBXTargetDependency */, CE88199A1F91B0DA000D5AB7 /* PBXTargetDependency */, @@ -212,10 +215,25 @@ CEBA3F8C2FF3F25E00758CA6 /* Hello.vert.metal in CopyFiles */ = {isa = PBXBuildFile; fileRef = HS00000000000000000000A2 /* Hello.vert.metal */; }; CEBCB6D521664EB1009E6289 /* Types.h in Headers */ = {isa = PBXBuildFile; fileRef = CEF1D8BB1D4704950099A857 /* Types.h */; settings = {ATTRIBUTES = (Public, ); }; }; CED40A8D304B2D700090C57A /* SDL3.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B1AA0001000000000000000A /* SDL3.xcframework */; }; + 27826E51CD9BF334A46F8B74 /* SDL3.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B1AA0001000000000000000A /* SDL3.xcframework */; }; + BB87084D13A301A5A22CBCA2 /* SDL3.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B1AA0001000000000000000A /* SDL3.xcframework */; }; + 98BFE75CAAA4EB3E01C703AE /* SDL3.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B1AA0001000000000000000A /* SDL3.xcframework */; }; CED40A8E304B2D700090C57A /* Objectively.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE73E9FA2FEEA5F60048BC24 /* Objectively.framework */; }; + C98EF9B35812D5CEDCD4285E /* Objectively.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE73E9FA2FEEA5F60048BC24 /* Objectively.framework */; }; + 254D1795460F9E287313E070 /* Objectively.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE73E9FA2FEEA5F60048BC24 /* Objectively.framework */; }; + 9BDEC2F3260285F60247A6D7 /* Objectively.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE73E9FA2FEEA5F60048BC24 /* Objectively.framework */; }; CED40A8F304B2D700090C57A /* ObjectivelyGPU.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CEEAAFF52FED7E6000FFEBE6 /* ObjectivelyGPU.framework */; }; + E85C0A9A11FE4DBF991CB55C /* ObjectivelyGPU.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CEEAAFF52FED7E6000FFEBE6 /* ObjectivelyGPU.framework */; }; + A45306E1A2B9F91C43A3D035 /* ObjectivelyGPU.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CEEAAFF52FED7E6000FFEBE6 /* ObjectivelyGPU.framework */; }; + 7C9B4270AF0FF5BBEBC68236 /* ObjectivelyGPU.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CEEAAFF52FED7E6000FFEBE6 /* ObjectivelyGPU.framework */; }; CED40A90304B2D700090C57A /* ObjectivelyMVC.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CED157CD1C4BF3AC00FBA2DE /* ObjectivelyMVC.framework */; }; + 53D16432B2CCCE4F3956685E /* ObjectivelyMVC.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CED157CD1C4BF3AC00FBA2DE /* ObjectivelyMVC.framework */; }; + A95FB52C4A3FB476D42F4A5E /* ObjectivelyMVC.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CED157CD1C4BF3AC00FBA2DE /* ObjectivelyMVC.framework */; }; + 66EA7319EF11A582FD1064E2 /* ObjectivelyMVC.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CED157CD1C4BF3AC00FBA2DE /* ObjectivelyMVC.framework */; }; CED40A96304B2D7F0090C57A /* ImageAtlas.c in Sources */ = {isa = PBXBuildFile; fileRef = CED40A86304B2D550090C57A /* ImageAtlas.c */; }; + 5F8A115B0069B76D5DF21618 /* Image.c in Sources */ = {isa = PBXBuildFile; fileRef = FEDCD6FF3EFA7A4388486410 /* Image.c */; }; + B25F30D62819F943DCBC5293 /* Text.c in Sources */ = {isa = PBXBuildFile; fileRef = A2A76A66C35A8EA25E73AED3 /* Text.c */; }; + DCB5AE1270D2C8B8C4E44027 /* TextView.c in Sources */ = {isa = PBXBuildFile; fileRef = 02533D62677918BD9C9D8632 /* TextView.c */; }; CED56FF62FE3440000756996 /* SDL3.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B1AA0001000000000000000A /* SDL3.xcframework */; }; CED56FF92FE3440000756996 /* SDL3.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B1AA0001000000000000000A /* SDL3.xcframework */; }; CEEAAFF62FED7E6000FFEBE6 /* ObjectivelyGPU.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CEEAAFF52FED7E6000FFEBE6 /* ObjectivelyGPU.framework */; }; @@ -345,6 +363,27 @@ remoteGlobalIDString = CED157CC1C4BF3AC00FBA2DE; remoteInfo = ObjectivelyMVC; }; + EDC8B69B4C254E0B20983FBD /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = CED157931C4BEA8900FBA2DE /* Project object */; + proxyType = 1; + remoteGlobalIDString = CED157CC1C4BF3AC00FBA2DE; + remoteInfo = ObjectivelyMVC; + }; + 6CE99E2EEC2CA9CACE23F199 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = CED157931C4BEA8900FBA2DE /* Project object */; + proxyType = 1; + remoteGlobalIDString = CED157CC1C4BF3AC00FBA2DE; + remoteInfo = ObjectivelyMVC; + }; + 7EE94EDE9901ED9472A2194D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = CED157931C4BEA8900FBA2DE /* Project object */; + proxyType = 1; + remoteGlobalIDString = CED157CC1C4BF3AC00FBA2DE; + remoteInfo = ObjectivelyMVC; + }; CED40A97304B2DFA0090C57A /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = CED157931C4BEA8900FBA2DE /* Project object */; @@ -352,6 +391,27 @@ remoteGlobalIDString = CED40A87304B2D700090C57A; remoteInfo = "ObjectivelyMVC-ImageAtlas"; }; + 53F472CDBD720DEE99451939 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = CED157931C4BEA8900FBA2DE /* Project object */; + proxyType = 1; + remoteGlobalIDString = 49B779A4A2BCD3DCD3C3BD06; + remoteInfo = "ObjectivelyMVC-Image"; + }; + 288C55F591D0A32135B0B1D8 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = CED157931C4BEA8900FBA2DE /* Project object */; + proxyType = 1; + remoteGlobalIDString = 563130823413BCCA09134760; + remoteInfo = "ObjectivelyMVC-Text"; + }; + 86612F2306FFCE232CE1BB04 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = CED157931C4BEA8900FBA2DE /* Project object */; + proxyType = 1; + remoteGlobalIDString = A73BC0FDBE1118F914A440F1; + remoteInfo = "ObjectivelyMVC-TextView"; + }; HUDA00000000000000000010 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = CED157931C4BEA8900FBA2DE /* Project object */; @@ -441,6 +501,33 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 9F0912651CABAEDD083DCF56 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 12; + dstPath = ""; + dstSubfolderSpec = 7; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F7CF640A30C7C28D712A607A /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 12; + dstPath = ""; + dstSubfolderSpec = 7; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3B8576F5164522B222E465B0 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 12; + dstPath = ""; + dstSubfolderSpec = 7; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ @@ -635,7 +722,13 @@ CED157B51C4BF32A00FBA2DE /* Makefile.am */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Makefile.am; sourceTree = ""; }; CED157CD1C4BF3AC00FBA2DE /* ObjectivelyMVC.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ObjectivelyMVC.framework; sourceTree = BUILT_PRODUCTS_DIR; }; CED40A86304B2D550090C57A /* ImageAtlas.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = ImageAtlas.c; sourceTree = ""; }; + FEDCD6FF3EFA7A4388486410 /* Image.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = Image.c; sourceTree = ""; }; + A2A76A66C35A8EA25E73AED3 /* Text.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = Text.c; sourceTree = ""; }; + 02533D62677918BD9C9D8632 /* TextView.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = TextView.c; sourceTree = ""; }; CED40A95304B2D700090C57A /* ObjectivelyMVC-ImageAtlas */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = "ObjectivelyMVC-ImageAtlas"; sourceTree = BUILT_PRODUCTS_DIR; }; + 4A4E9B8FCA4EC2C5AF4D4C30 /* ObjectivelyMVC-Image */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = "ObjectivelyMVC-Image"; sourceTree = BUILT_PRODUCTS_DIR; }; + C3EB55F6FC6630DE5644B5A7 /* ObjectivelyMVC-Text */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = "ObjectivelyMVC-Text"; sourceTree = BUILT_PRODUCTS_DIR; }; + 26EA4C5E6BBC55A50927A783 /* ObjectivelyMVC-TextView */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = "ObjectivelyMVC-TextView"; sourceTree = BUILT_PRODUCTS_DIR; }; CEEAAFF52FED7E6000FFEBE6 /* ObjectivelyGPU.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = ObjectivelyGPU.framework; sourceTree = BUILT_PRODUCTS_DIR; }; CEF1D88B1D4265A70099A857 /* Panel.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = Panel.c; sourceTree = ""; }; CEF1D88C1D4265A70099A857 /* Panel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = Panel.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; @@ -790,6 +883,45 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 86088652F18876526D6A2EB3 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + CE0C221C304CC285008A6A2A /* SDL3_image.xcframework in Frameworks */, + CE0C221D304CC285008A6A2A /* SDL3_ttf.xcframework in Frameworks */, + 27826E51CD9BF334A46F8B74 /* SDL3.xcframework in Frameworks */, + C98EF9B35812D5CEDCD4285E /* Objectively.framework in Frameworks */, + E85C0A9A11FE4DBF991CB55C /* ObjectivelyGPU.framework in Frameworks */, + 53D16432B2CCCE4F3956685E /* ObjectivelyMVC.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DB3CFD7C69B22D18B0E48944 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + CE0C221C304CC285008A6A2A /* SDL3_image.xcframework in Frameworks */, + CE0C221D304CC285008A6A2A /* SDL3_ttf.xcframework in Frameworks */, + BB87084D13A301A5A22CBCA2 /* SDL3.xcframework in Frameworks */, + 254D1795460F9E287313E070 /* Objectively.framework in Frameworks */, + A45306E1A2B9F91C43A3D035 /* ObjectivelyGPU.framework in Frameworks */, + A95FB52C4A3FB476D42F4A5E /* ObjectivelyMVC.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E42EC99E530339388B01BE5D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + CE0C221C304CC285008A6A2A /* SDL3_image.xcframework in Frameworks */, + CE0C221D304CC285008A6A2A /* SDL3_ttf.xcframework in Frameworks */, + 98BFE75CAAA4EB3E01C703AE /* SDL3.xcframework in Frameworks */, + 9BDEC2F3260285F60247A6D7 /* Objectively.framework in Frameworks */, + 7C9B4270AF0FF5BBEBC68236 /* ObjectivelyGPU.framework in Frameworks */, + 66EA7319EF11A582FD1064E2 /* ObjectivelyMVC.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; HUDA00000000000000000009 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -939,10 +1071,13 @@ isa = PBXGroup; children = ( CE0C2206304CBC73008A6A2A /* Font+Bitmap.c */, + FEDCD6FF3EFA7A4388486410 /* Image.c */, CED40A86304B2D550090C57A /* ImageAtlas.c */, CE55C1BB1F944C1B00D5A326 /* Selector.c */, CE8819891F91B094000D5AB7 /* Style.c */, CE34C8941FB29F3A0025F231 /* Stylesheet.c */, + A2A76A66C35A8EA25E73AED3 /* Text.c */, + 02533D62677918BD9C9D8632 /* TextView.c */, 58441034DC162E9BE53173F4 /* View.c */, CE423A2C1F534127002767E7 /* Makefile.am */, ); @@ -1092,6 +1227,9 @@ B1AA000100000000000000ED /* ObjectivelyMVC-Hello-iOS.app */, 84BBDF9467CD6F80ACF8AADF /* ObjectivelyMVC-View */, CED40A95304B2D700090C57A /* ObjectivelyMVC-ImageAtlas */, + 4A4E9B8FCA4EC2C5AF4D4C30 /* ObjectivelyMVC-Image */, + C3EB55F6FC6630DE5644B5A7 /* ObjectivelyMVC-Text */, + 26EA4C5E6BBC55A50927A783 /* ObjectivelyMVC-TextView */, CE0C2215304CBC82008A6A2A /* ObjectivelyMVC-Font+Bitmap */, ); name = Products; @@ -1334,6 +1472,60 @@ productReference = CED40A95304B2D700090C57A /* ObjectivelyMVC-ImageAtlas */; productType = "com.apple.product-type.tool"; }; + 49B779A4A2BCD3DCD3C3BD06 /* ObjectivelyMVC-Image */ = { + isa = PBXNativeTarget; + buildConfigurationList = D44656D0AEE1A511C175339A /* Build configuration list for PBXNativeTarget "ObjectivelyMVC-Image" */; + buildPhases = ( + 9B8AE887D35458AE7C0EE060 /* Sources */, + 86088652F18876526D6A2EB3 /* Frameworks */, + 9F0912651CABAEDD083DCF56 /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + C2DC6F82A5128ED1D64A0D28 /* PBXTargetDependency */, + ); + name = "ObjectivelyMVC-Image"; + productName = "ObjectivelyMVC-Image"; + productReference = 4A4E9B8FCA4EC2C5AF4D4C30 /* ObjectivelyMVC-Image */; + productType = "com.apple.product-type.tool"; + }; + 563130823413BCCA09134760 /* ObjectivelyMVC-Text */ = { + isa = PBXNativeTarget; + buildConfigurationList = 42DA443CE43697A0CE2778F7 /* Build configuration list for PBXNativeTarget "ObjectivelyMVC-Text" */; + buildPhases = ( + 243313ABE31DC93EA936F339 /* Sources */, + DB3CFD7C69B22D18B0E48944 /* Frameworks */, + F7CF640A30C7C28D712A607A /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + BA04F7324C595E256D51814C /* PBXTargetDependency */, + ); + name = "ObjectivelyMVC-Text"; + productName = "ObjectivelyMVC-Text"; + productReference = C3EB55F6FC6630DE5644B5A7 /* ObjectivelyMVC-Text */; + productType = "com.apple.product-type.tool"; + }; + A73BC0FDBE1118F914A440F1 /* ObjectivelyMVC-TextView */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7935DBBB394E57382160DE99 /* Build configuration list for PBXNativeTarget "ObjectivelyMVC-TextView" */; + buildPhases = ( + FE5F77E35CE051DA2EE56CCD /* Sources */, + E42EC99E530339388B01BE5D /* Frameworks */, + 3B8576F5164522B222E465B0 /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + 10B40536A4C6ED7844DECE20 /* PBXTargetDependency */, + ); + name = "ObjectivelyMVC-TextView"; + productName = "ObjectivelyMVC-TextView"; + productReference = 26EA4C5E6BBC55A50927A783 /* ObjectivelyMVC-TextView */; + productType = "com.apple.product-type.tool"; + }; HUDA00000000000000000015 /* ObjectivelyMVC-HUD */ = { isa = PBXNativeTarget; buildConfigurationList = HUDA00000000000000000014 /* Build configuration list for PBXNativeTarget "ObjectivelyMVC-HUD" */; @@ -1393,6 +1585,9 @@ CED157CC1C4BF3AC00FBA2DE /* ObjectivelyMVC */, CE0C2207304CBC82008A6A2A /* ObjectivelyMVC-Font+Bitmap */, CED40A87304B2D700090C57A /* ObjectivelyMVC-ImageAtlas */, + 49B779A4A2BCD3DCD3C3BD06 /* ObjectivelyMVC-Image */, + 563130823413BCCA09134760 /* ObjectivelyMVC-Text */, + A73BC0FDBE1118F914A440F1 /* ObjectivelyMVC-TextView */, CE55C1BC1F944C6D00D5A326 /* ObjectivelyMVC-Selector */, CE88198A1F91B0BE000D5AB7 /* ObjectivelyMVC-Style */, CE34C8951FB29F700025F231 /* ObjectivelyMVC-Stylesheet */, @@ -1433,7 +1628,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "cd $BUILT_PRODUCTS_DIR &&\n./ObjectivelyMVC-Font+Bitmap &&\n./ObjectivelyMVC-ImageAtlas &&\n./ObjectivelyMVC-Selector &&\n./ObjectivelyMVC-Style &&\n./ObjectivelyMVC-Stylesheet &&\n./ObjectivelyMVC-View\n"; + shellScript = "cd $BUILT_PRODUCTS_DIR &&\n./ObjectivelyMVC-Font+Bitmap &&\n./ObjectivelyMVC-Image &&\n./ObjectivelyMVC-ImageAtlas &&\n./ObjectivelyMVC-Selector &&\n./ObjectivelyMVC-Style &&\n./ObjectivelyMVC-Stylesheet &&\n./ObjectivelyMVC-Text &&\n./ObjectivelyMVC-TextView &&\n./ObjectivelyMVC-View\n"; }; CE92C5181FE02B5C00FDB0DF /* Preprocess Assets */ = { isa = PBXShellScriptBuildPhase; @@ -1671,6 +1866,30 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 9B8AE887D35458AE7C0EE060 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 5F8A115B0069B76D5DF21618 /* Image.c in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 243313ABE31DC93EA936F339 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B25F30D62819F943DCBC5293 /* Text.c in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + FE5F77E35CE051DA2EE56CCD /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DCB5AE1270D2C8B8C4E44027 /* TextView.c in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; HUDA00000000000000000008 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -1755,11 +1974,44 @@ target = CED157CC1C4BF3AC00FBA2DE /* ObjectivelyMVC */; targetProxy = CED40A89304B2D700090C57A /* PBXContainerItemProxy */; }; + C2DC6F82A5128ED1D64A0D28 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = ObjectivelyMVC; + target = CED157CC1C4BF3AC00FBA2DE /* ObjectivelyMVC */; + targetProxy = EDC8B69B4C254E0B20983FBD /* PBXContainerItemProxy */; + }; + BA04F7324C595E256D51814C /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = ObjectivelyMVC; + target = CED157CC1C4BF3AC00FBA2DE /* ObjectivelyMVC */; + targetProxy = 6CE99E2EEC2CA9CACE23F199 /* PBXContainerItemProxy */; + }; + 10B40536A4C6ED7844DECE20 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = ObjectivelyMVC; + target = CED157CC1C4BF3AC00FBA2DE /* ObjectivelyMVC */; + targetProxy = 7EE94EDE9901ED9472A2194D /* PBXContainerItemProxy */; + }; CED40A98304B2DFA0090C57A /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = CED40A87304B2D700090C57A /* ObjectivelyMVC-ImageAtlas */; targetProxy = CED40A97304B2DFA0090C57A /* PBXContainerItemProxy */; }; + B3DD5991CDBD4582092CC4F6 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 49B779A4A2BCD3DCD3C3BD06 /* ObjectivelyMVC-Image */; + targetProxy = 53F472CDBD720DEE99451939 /* PBXContainerItemProxy */; + }; + 00E123F744F252F431ACB5AB /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 563130823413BCCA09134760 /* ObjectivelyMVC-Text */; + targetProxy = 288C55F591D0A32135B0B1D8 /* PBXContainerItemProxy */; + }; + 0850FEAA7E79310A5E7C5ED5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = A73BC0FDBE1118F914A440F1 /* ObjectivelyMVC-TextView */; + targetProxy = 86612F2306FFCE232CE1BB04 /* PBXContainerItemProxy */; + }; HUDA00000000000000000011 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = CED157CC1C4BF3AC00FBA2DE /* ObjectivelyMVC */; @@ -2136,6 +2388,39 @@ }; name = Debug; }; + F63BAEE767957C57E6DF9242 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + OTHER_LDFLAGS = ( + "$(inherited)", + "-lcheck", + ); + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + A99FE10786B92347B082A9F3 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + OTHER_LDFLAGS = ( + "$(inherited)", + "-lcheck", + ); + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 9C6C61A5C7F83CC04149A29D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + OTHER_LDFLAGS = ( + "$(inherited)", + "-lcheck", + ); + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; CED40A94304B2D700090C57A /* Release */ = { isa = XCBuildConfiguration; buildSettings = { @@ -2147,6 +2432,39 @@ }; name = Release; }; + 0C2EC2CD51D9FF5799F74AB7 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + OTHER_LDFLAGS = ( + "$(inherited)", + "-lcheck", + ); + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; + 7FC537B8903F19812820D170 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + OTHER_LDFLAGS = ( + "$(inherited)", + "-lcheck", + ); + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; + 8F015166B33C65189975EF38 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + OTHER_LDFLAGS = ( + "$(inherited)", + "-lcheck", + ); + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; HUDA00000000000000000012 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -2292,6 +2610,33 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + D44656D0AEE1A511C175339A /* Build configuration list for PBXNativeTarget "ObjectivelyMVC-Image" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F63BAEE767957C57E6DF9242 /* Debug */, + 0C2EC2CD51D9FF5799F74AB7 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 42DA443CE43697A0CE2778F7 /* Build configuration list for PBXNativeTarget "ObjectivelyMVC-Text" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A99FE10786B92347B082A9F3 /* Debug */, + 7FC537B8903F19812820D170 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7935DBBB394E57382160DE99 /* Build configuration list for PBXNativeTarget "ObjectivelyMVC-TextView" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 9C6C61A5C7F83CC04149A29D /* Debug */, + 8F015166B33C65189975EF38 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; HUDA00000000000000000014 /* Build configuration list for PBXNativeTarget "ObjectivelyMVC-HUD" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/ObjectivelyMVC.xcodeproj/xcshareddata/xcschemes/ObjectivelyMVC-Image.xcscheme b/ObjectivelyMVC.xcodeproj/xcshareddata/xcschemes/ObjectivelyMVC-Image.xcscheme new file mode 100644 index 00000000..f8db7f75 --- /dev/null +++ b/ObjectivelyMVC.xcodeproj/xcshareddata/xcschemes/ObjectivelyMVC-Image.xcscheme @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ObjectivelyMVC.xcodeproj/xcshareddata/xcschemes/ObjectivelyMVC-Text.xcscheme b/ObjectivelyMVC.xcodeproj/xcshareddata/xcschemes/ObjectivelyMVC-Text.xcscheme new file mode 100644 index 00000000..0b237172 --- /dev/null +++ b/ObjectivelyMVC.xcodeproj/xcshareddata/xcschemes/ObjectivelyMVC-Text.xcscheme @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ObjectivelyMVC.xcodeproj/xcshareddata/xcschemes/ObjectivelyMVC-TextView.xcscheme b/ObjectivelyMVC.xcodeproj/xcshareddata/xcschemes/ObjectivelyMVC-TextView.xcscheme new file mode 100644 index 00000000..5257adf1 --- /dev/null +++ b/ObjectivelyMVC.xcodeproj/xcshareddata/xcschemes/ObjectivelyMVC-TextView.xcscheme @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 2f1c3da79bdf9a90c5b6ae0e60179347336d90a0 Mon Sep 17 00:00:00 2001 From: Jay Dolan Date: Fri, 25 Sep 2026 10:39:45 -0400 Subject: [PATCH 09/11] Remove every option in Select::removeAllOptions The options were removed while the array was enumerated, which skipped every other option, so a Select that was cleared and filled again kept stale options. Co-Authored-By: Claude Opus 5.5 (1M context) --- Sources/ObjectivelyMVC/Select.c | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/Sources/ObjectivelyMVC/Select.c b/Sources/ObjectivelyMVC/Select.c index 14d9fc1f..5444aa54 100644 --- a/Sources/ObjectivelyMVC/Select.c +++ b/Sources/ObjectivelyMVC/Select.c @@ -337,19 +337,18 @@ static Option *optionWithValue(const Select *self, const ident value) { return $((Array *) self->options, find, optionWithValue_predicate, value); } -/** - * @brief ArrayEnumerator for removeAllOptions. - */ -static void removeAllOptions_enumerate(const Array *array, ident obj, ident data) { - $((Select *) data, removeOption, obj); -} - /** * @fn void Select::removeAllOptions(Select *self) * @memberof Select + * @remarks The options are removed from the last, because removing an option while the array is + * enumerated skips the option after it. */ static void removeAllOptions(Select *self) { - $((Array *) self->options, enumerate, removeAllOptions_enumerate, self); + + Option *option; + while ((option = $((Array *) self->options, lastObject))) { + $(self, removeOption, option); + } } /** From b38e2c3fdc01c6ec50f127fffdb0a991bec99ae8 Mon Sep 17 00:00:00 2001 From: Jay Dolan Date: Fri, 25 Sep 2026 20:52:35 -0400 Subject: [PATCH 10/11] Fix stretch at a max size and when a style changes it A stretched subview that its max size clamped still advanced the stack by its full share, which left a gap. It now advances by its real size, and the unused share goes to the next stretched subview. A style change to stretch now lays out the superview again. Co-Authored-By: Claude Opus 5.5 (1M context) --- Sources/ObjectivelyMVC/StackView.c | 17 +++++++++++++++-- Sources/ObjectivelyMVC/View.c | 3 ++- Tests/ObjectivelyMVC/View.c | 28 ++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/Sources/ObjectivelyMVC/StackView.c b/Sources/ObjectivelyMVC/StackView.c index 4fa3629d..3713b734 100644 --- a/Sources/ObjectivelyMVC/StackView.c +++ b/Sources/ObjectivelyMVC/StackView.c @@ -184,7 +184,8 @@ static void layoutSubviews(View *self) { break; } - if (subview->stretch && remaining) { + const bool stretch = subview->stretch && remaining; + if (stretch) { const int share = --numStretch ? remaining / (numStretch + 1) : remaining; switch (this->axis) { case StackViewAxisVertical: @@ -194,11 +195,23 @@ static void layoutSubviews(View *self) { subviewSize.w += share; break; } - remaining -= share; } $(subview, layoutWithSize, &subviewSize); + if (stretch) { + switch (this->axis) { + case StackViewAxisVertical: + remaining -= max(subview->frame.h - sizes[i].h, 0); + subviewSize.h = subview->frame.h; + break; + case StackViewAxisHorizontal: + remaining -= max(subview->frame.w - sizes[i].w, 0); + subviewSize.w = subview->frame.w; + break; + } + } + // Align along secondary axis switch (this->axis) { diff --git a/Sources/ObjectivelyMVC/View.c b/Sources/ObjectivelyMVC/View.c index 14e860df..900c5126 100644 --- a/Sources/ObjectivelyMVC/View.c +++ b/Sources/ObjectivelyMVC/View.c @@ -309,10 +309,11 @@ static void applyStyle(View *self, const Style *style) { ); const ViewVisibility visibility = self->visibility; + const bool stretch = self->stretch; $(self, bind, inlets, style->attributes); - if (self->visibility != visibility && self->superview) { + if ((self->visibility != visibility || self->stretch != stretch) && self->superview) { $(self->superview, setNeedsLayout); } diff --git a/Tests/ObjectivelyMVC/View.c b/Tests/ObjectivelyMVC/View.c index ea0ab85b..3ec74f7e 100644 --- a/Tests/ObjectivelyMVC/View.c +++ b/Tests/ObjectivelyMVC/View.c @@ -417,6 +417,33 @@ START_TEST(stretchChildrenShareRemainingWidth) { } END_TEST +START_TEST(stretchChildAtMaxSizeLeavesRemainderToNext) { + + StackView *stackView = $(alloc(StackView), initWithFrame, NULL); + stackView->view.minSize = stackView->view.maxSize = MakeSize(110, 10); + stackView->axis = StackViewAxisHorizontal; + + View *a = fixedView(10, 10); + View *b = flexibleView(0, 10); + View *c = flexibleView(0, 10); + b->stretch = c->stretch = true; + b->maxSize.w = 30; + + $((View *) stackView, addSubview, a); + $((View *) stackView, addSubview, b); + $((View *) stackView, addSubview, c); + + $((View *) stackView, layoutIfNeeded); + + ck_assert_int_eq(30, b->frame.w); + ck_assert_int_eq(10, b->frame.x); + ck_assert_int_eq(70, c->frame.w); + ck_assert_int_eq(40, c->frame.x); + + release(stackView); + +} END_TEST + START_TEST(stretchIsIgnoredWithoutRemainingSpace) { StackView *stackView = $(alloc(StackView), initWithFrame, NULL); @@ -525,6 +552,7 @@ int main(int argc, char **argv) { tcase_add_test(tcase, pointerEventsPassThroughToSubviewsAndSiblings); tcase_add_test(tcase, stretchChildTakesRemainingHeight); tcase_add_test(tcase, stretchChildrenShareRemainingWidth); + tcase_add_test(tcase, stretchChildAtMaxSizeLeavesRemainderToNext); tcase_add_test(tcase, stretchIsIgnoredWithoutRemainingSpace); tcase_add_test(tcase, stretchContainChildKeepsStretchedSize); tcase_add_test(tcase, styledVisibilityRelaysOutSuperview); From 04429cdb2ae659c3e489b85035cea0cc624a1459 Mon Sep 17 00:00:00 2001 From: Jay Dolan Date: Fri, 25 Sep 2026 20:52:35 -0400 Subject: [PATCH 11/11] Move the key responder with keypad Tab TextView already completes with keypad Tab, but WindowController moved focus only for Tab. Co-Authored-By: Claude Opus 5.5 (1M context) --- Sources/ObjectivelyMVC/WindowController.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ObjectivelyMVC/WindowController.c b/Sources/ObjectivelyMVC/WindowController.c index 7b80d8d6..a11ddb12 100644 --- a/Sources/ObjectivelyMVC/WindowController.c +++ b/Sources/ObjectivelyMVC/WindowController.c @@ -351,7 +351,7 @@ static void respondToEvent(WindowController *self, const SDL_Event *event) { } if (event->type == SDL_EVENT_KEY_DOWN) { - if (event->key.key == SDLK_TAB) { + if (event->key.key == SDLK_TAB || event->key.key == SDLK_KP_TAB) { View *current = $(self, keyResponder);