В предыдущей части было сделано поведение ползунков:
На этот раз, по следам дискуссии в комментах, я решил сделать скроллинг контента внутри окна. И я его даже сделал, но. Возникло большое "но".
Посмотрим на такую конфигурацию:
Три цветных квадратика по горизонтали это группа элементов, которая прокручивается влево и вправо с помощью своего горизонтального скроллбара.
Таких групп может быть две и более, и каждая прокручивается независимо собственным скроллбаром.
Эти группы объединяются в более крупную группу, которая прокручивается уже вверх и вниз с помощью вертикального скроллбара справа.
Всё это вписывается в текущую схему, пока мы не начнём перетаскивать группы. То есть двигать их не с помощью скроллбара, а просто наводя указатель на группу и перетаскивая её.
Вообще я не предусматривал именно такой способ управления в своём GUI, но раз уж его упомянули, то надо сделать и его.
Представим, что мы навели указатель на одну из горизонтальных групп. Но мы также попали в вертикальную группу, потому что горизонтальная находится внутри вертикальной. Но активный элемент, которому выпало обработать событие – тот, который нашли последним в иерархии, то есть горизонтальная группа.
Этот элемент откликается на событие перетаскивания, но специфически. Он будет перетаскиваться только тогда, когда перемещение происходит по горизонтали.
Если же оно происходит по вертикали, такой элемент ничего не будет делать. Вместо него обработку должна произвести вертикальная группа.
Но она уже не может, потому что с событием сейчас разбирается не она, а её потомок.
Проблема возникает из-за того, что обработкой события в моей схеме может заниматься только один элемент, а здесь их два, и событие делится между ними. Каждый обрабатывает свою часть.
Таким образом, назрела необходимость мульти-обработки события двумя и более элементами.
Рассмотрим традиционный путь.
Находим самый первый от корня элемент, который может обработать событие. Это будет вертикальная группа.
Вертикальная группа обрабатывает событие. У него общий тип "перетаскивание", но из него берётся только вертикальная составляющая.
Далее группа передаёт это событие своим детям, которые также могли бы его обработать. Если потомок это горизонтальная группа, то она обработает то же самое событие, но возьмёт только горизонтальную составляющую перемещения. Затем событие отдаётся следущему потомку и т.д.
Таким образом, каждому элементу, попадающему под событие, будет дана возможность его обработать по-своему или просто пропустить дальше.
Но решение одной проблемы порождает другие.
Если у нас есть два вложенных элемента, каждый из которых отрабатывает перетаскивание в любом направлении, то перетаскивание элемента-потомка приведёт к аналогичному перетаскиванию элемента-родителя. Что чаще всего нас устраивать не будет. Когда мы указываем на потомка, обычно мы хотим, чтобы перемещался только он.
Но указывая на потомка, мы неминуемо укажем и на его родителя, потому что поиск происходит от корня. И родитель тоже обработает перетаскивание.
Следовательно, начинать обработку надо всё-таки от самого последнего потомка. И только если он каким-то образом не завершил её до конца, передавать её наверх к родителю.
Рассмотрим, как будет происходить обработка в обоих из вышеописанных случаев.
В логике элемента "горизонтальная группа" прописано делать горизонтальное перемещение, но после этого отдавать событие своему родителю. Родитель – вертикальная группа – обработает вертикальное перемещение и больше никуда передавать событие не будет.
Но возможен и другой вариант, когда горизонтальная группа это родитель, а вертикальная – потомок, и их логика передачи событий поменяется местами. Следовательно, логика передачи события должна зависеть не от типа элемента, а от его настройки внутри UI. То есть в свойствах элемента GUI_Item нужно будет руками прописать, отдаёт он событие наверх или нет.
В сущности, когда мы собираем GUI, так и должно быть. Интерфейс не может решить за нас, как и на что реагировать.
Тогда второй случай не представляет проблем. Если мы попали в потомка, он обработает перемещение и в соответствии со своей настройкой ничего не будет передавать наверх.
Поиск родителя
Ранее это делалось с помощью элемента GUI_ItemNode, в котором находился индекс текущего элемента и ссылка на его родителя.
Пораскинув мозгами, я нашёл вариант попроще. В структуру GUI_ItemRecord добавляется одно поле parent_offset:
Оно указывает, на каком расстоянии от индекса элемента находится индекс родителя. Считать его руками не надо, оно легко посчитается во время добавления элемента в дерево.
Чтобы найти родителя, надо от текущего индекса элемента отнять parent_offset, и всё. Необходимость в структуре GUI_ItemNode полностью отпала.
Изменение схемы обработки
Текущая схема стала не очень удобной, так как точек вызова обработки было несколько. Она отталкивалась от состояния диспетчера, я переделал её на типы события. Разницы в логике нет, это просто одни условия были выше других, а теперь стали ниже. Но предыдущая схема была чуть оптимизирована, например не искала элемент в дереве, когда это не надо, и не вызывала его обработку, когда не надо.
В новой схеме, ради лучшей структурированности, целевой элемент ищется всегда и обрабатывается всегда. Это позволило сделать одну точку входа в обработку для всех событий и всех типов элементов.
Далее обработка расходится на специализированные функции для каждого типа элемента, которые я поместил в отдельные файлы вместе с определениями типов элементов, так что искать их стало легче.
Рекурсивный возврат события
Ну и главное изменение это то, что после завершения обработки ничего ещё не заканчивается. У структуры GUI_Item добавлено поле return_state:
Элемент может вообще не обрабатывать какой-то тип события, но может возвращать его наверх. Этот тип указывается в return_state.
Если у элемента заполнено поле return_state, и оно совпадает с типом обрабатываемого события, то ищется родитель элемента и обработка рекурсивно запускается уже с родителем.
Демо
Я добавил тип элемента GUI_ContentPane, это просто группа, которую можно перетаскивать по горизонтали, вертикали или как угодно – вместе со всеми её детьми.
И добавил тип GUI_Placeholder, это просто цветной квадратик, чтобы сейчас можно было сделать какую-то демонстрацию. Квадратик умеет рисоваться так, чтобы не выходить за пределы родительской группы.
Далее я сделал 2 горизонтальных группы по 3 квадратика и одну вертикальную, которая их объединяет.
И настроил возвраты событий так:
- квадратики не обрабатывают ничего, но возвращают событие перетаскивания
- обе горизонтальные группы обрабатывают событие перетаскивания только по горизонтали и возвращают его
- вертикальная группа обрабатывает событие перетаскивания по вертикали и ничего не возвращает.
Теперь, если я попал курсором в квадратик и начал тащить, он вернёт это событие своей горизонтальной группе-родителю, а та в свою очередь вернёт его вертикальной группе-родителю. Если я попал мимо квадратика, но в горизонтальную группу, то обработка будет начинаться с неё и достигать вертикальной группы. Если я попал мимо обоих горизонтальных групп, но в вертикальную, обрабатываться будет только она.
Видео работы:
Осталось приделать ползунки для перемещения. Это всё уже было, но из-за изменения схемы обработки я откатил всё назад, да и сама тема составных элементов потребует отдельного обсуждения.
Код лежит на гитхабе:
Читайте дальше: