gug-bg-herd
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Re: Аналог на GDK_INVERT при cairo/GTK 3


From: Kaloian Doganov
Subject: Re: Аналог на GDK_INVERT при cairo/GTK 3
Date: Mon, 11 Nov 2019 22:50:26 +0200
User-agent: Evolution 3.30.5-1.1

On Mon, 2019-11-11 at 01:40 +0200, Yavor Doganov wrote:

> Проблемът е, че при движението на мишката правоъгълника се променя и
> нещо трябва да „трие“ следите.  Точно това прави dovtk-lasso, което
> ми
> изглежа доста сложно, за моя мозък поне.  Състоянието се записва в
> серия правоъгълници, които се опресняват при движението,
> възстановявайки основата.  Това GDK_INVERT го прави автоматично и то
> "under the hood", както се казва.

Използването на GDK_INVERT е хитър трик защото ако на това място няма
селекция, GDK_INVERT я рисува (и то с добър контраст), а ако има -- я
трие.

Тази (или всяка аналогична на GDK_INVERT) техника, обаче, разчита на
това, че в даден момент знаем дали на екрана има нарисувана селекция
или не.  Тъй като ::draw никога не рисува селекцията, а ::draw може да
се случи по всяко време, това може да ни постави в ситуация при
движение на мишката да "изтрием" селекция, която не е там.  В този
случай GDK_INVERT (или аналогът му в Cairo, ако има такъв) ще нарисува
селекция, вместо да я изтрие.  След като сме "изтрили" старата
селекция, ще тръгнем да рисуваме актуалната, пак с GDK_INVERT.  Това ще
доведе до втора селекция на екрана.  Но тъй като старата и новата
селекция съвпадат донякъде (поне в началната си точка, обикновено и
повече), а ние рисуваме с GDK_INVERT, сега ще инвертираме някои от
пискелите, които току-що сме инвертирали докато мислено трихме старата
селекция (а в действителност я рисувахме).  Сега практически ще изтрием
част от наистина активната селекция.  Визуално, резултатът е грозен
артефакт на екрана.  Всичко това е демонстрирано в screenshot-ите по-
долу.

Иронията е, че това разминаване нямаше да се случи ако освен всичко
друго ::draw рисуваше и селекцията.

> > Едва сега попрегледах кода в gtkplotcanvas.c и, честно казано, ми
> > се
> > струва, че логиката около рисуването на widget-а поначало е
> > некоректна.
> > Авторът има имплементация на draw, която не рисува пълният state на
> > widget-а.  Вместо това изглежда, че draw рисува базовата графика, а
> > после авторът рисува допълнително в резултат от някакви събития
> > (напр.
> > движение на мишката).  Това е доста кофти, защото ако draw бъде
> > извикан
> > по друг повод, нарисуваното на екрана ще излезе от синхрон с
> > потребителския state.
> 
> Да, това е типична практика при много програми, писани за GTK 2 (и
> 1.2, BTW), работи си без проблем.  Понеже събитието „провлачване с
> мишка“ се инвалидира при почти всяко друго събитие, това не е голям
> кахър.

Ако това е типична практика, значи масово не са се спазвали правилата
на GUI framework-а (в случая -- GTK, но в това отношение същите
принципи важат във всеки популярен GUI framework от 90-те години
насам). GTK казва, че всеки GtkWidget трябва да може да нарисува себе
си при ::draw.  В случая имаме GtkPlotCanvas, който се обявява за
GtkWidget, но не е в състояние да нарисува себе си при ::draw.  Това е
неочаквано за framework-а, който е изграден така, че да разчита на това
свойство на widget-ите.

Селекцията е част от state-а на GtkPlotCanvas, но GtkPlotCanvas::draw
не рисува селекцията, а само това, което е под нея.  Това е все едно
GtkEntry да рисува само рамките на текстовото поле, а самият текст да
се рисува от event handler-а за натискане на клавиш.  Прерисуването на
GtkEntry ще води до видима загуба на написания текст, и потребителят ще
трябва да натисне още един клавиш, за да види нещо от написаното. 

Нарушаването на правилата на framework-а не винаги води до
катастрофални последици, но често води до програми, които работят "общо
взето", "в повечето случаи", или само в "типичния случай".  Според мен
това е недопустимо, ако коректното решение не води до грандиозна
сложност.  А в случая коректното решение е по-просто, отколкото си
представяш.  Ще се опитам да го обясня подробно по-долу.

> > Ето един пример [...] селекцията се одрисква.
> 
> Пробва ли го?  Аз съм на Window Maker и нямам клавишна комбинация за
> максимизиране, но пробвах с Alt-Tab да положа друг прозорец върху
> графиката -- това незабавано инвалидира селекцията и не се случва
> нищо, просто пунктирания правоъгълник изчезва.

Пробвах го на GNOME 3 и възпроизведох проблема от първия път.  При
всеки следващ опит проблемът се възпроизвежда устойчиво.  Прилагам
screenshot-и от 1) преди Maximize, 2) след Maximize, 3) след Restore
(обратното на Maximize), и 4) след като съм натиснал ESC и съм отпуснал
бутона на мишката.  Обърни внимание, че в този момент вече нямам
активна селекция според вътрешното състояние на програмата, но имам
изрисувана селекция на екрана.  Това е артефакт от лошото управление на
това вътрешно състояние и неговото отражение върху екрана.  Артефактът
изчезва ако нещо предизвика ::draw -- напр. отново Maximize или пък от
менюто избера Zoom | Reset to original zoom.

> > Този ефект е пряко следствие от липсата на читав draw, който да
> > отразява текущия модел върху екрана. Изображението на екрана се
> > получава от основния draw, плюс още нафлякани неща върху него които
> > се
> > рисуват не по време на draw -- при движение на мишката се трие
> > старата
> > селекция, рисува се нова, т.н. 
> 
> Дори и да се чертае само от ::draw, това няма как да промени нещата и
> няма как да ме улесни по никакъв начин.  На практика
> gdk_window_begin_draw_frame чертае в offscreen буфер, който след
> gdk_window_end_draw_frame се рисува ефективно на екрана.  Ако чертая
> само от ::draw и изпълнявам draw_selection оттам на базата на някаква
> глобална булева променлива, която се контролира от event handler-ите,
> това несъмнено е по-добре, но няма как да реши проблема с
> възстановяването на основната графика при движението на мишката.

Ако рисуването на widget-а включва и рисуването на селекцията (ако има
активна такава), и в същото време прерисуваш напълно widget-а при
движение на мишката, това ще реши проблема ти.  Няма да има нужда да
триеш от екрана старото състояние на селекцията, понеже то ще се
забърсва при изрисуването на графиката.  В отношението между графика и
селекция, концептуално графиката се явява "фон" (background), а
селекцията седи отгоре (foreground).  Всеки път като изрисуваме
background-а наново, след това веднага трябва да рисуваме и foreground-
а, в този ред.  (Ако изрисуваме първо foreground-а, а после background-
а, foreground-а ще изчезне от екрана, както става в твоя примерен код.)

Това, което ти предлагам, е да сложиш рисуването на активната селекция
на последно място в ::draw.  Според мен няма нужда от
g_signal_connect_after, защото ти така или иначе можеш да промениш
имплементацията на ::draw.  Струва ми се, че g_signal_connect_after е
похват за свързване на widget-и, на които не можем (или не искаме) да
пипаме оригиналната имплементация.  След като draw започне да рисува
активната селекция, просто караш събитията, които в момента променят
състоянието на селекцията, да продължат да променят вътрешното
състояние на селекцията, но да викат draw или queue_draw вместо да
рисуват сами.  Идеята е всичкото рисуване да се остави на draw, а
когато няма активна селекция, движението на мишката да не предизвиква
draw.  Това е.  Не само, че не е много сложно, ами е по-просто от това,
което GtkPlotCanvas прави в момента.

По отношение на глобалната булева променлива, не съм сигурен дали имаш
предвид това, което аз разбирам.  Дали има да се рисува селекция или не
е част от състоянието на конкретната инстанция на GtkPlotCanvas, което
не бива да се пази в наистина глобална променлива.  (Иначе това
състояние ще се споделя между всички инстанции на GtkPlotCanvas, което
би било некоректно.)  На логическо ниво, за да може GtkPlotCanvas да
знае дали да рисува селекция при ::draw, му е необходимо да знае:

 - има ли активна селекция в момента
 - какви са нейните координати (GdkRectangle)

Като гледам типа GtkPlotCanvasAction, навярно GtkPlotCanvas вече има
всичко необходимо за да знае какво да прави при ::draw.

> > Най-простият начин да се организират тези неща е събитията да водят
> > до
> > смяна на някакъв невидим state в widget-а (т.нар. модел), а draw да
> > рисува на базата на този невидим state.  Напр. event handler-а за
> > движение на мишката трябва само да сменя някакви числови координати
> > в
> > модела на текущата селекция и да тригерира draw (независимо дали
> > синхронно с draw или асинхронно с queue_draw), който да ги
> > нарисува. 
> > Това е най-простият начин да се осигури кохерентност.
> 
> Това прави dovtk-lasso, хвърли едно око на кода.  Искаше ми се точно
> тази сложност да избегна.

Още не съм разгадал напълно как работи dovtk-lasso, но като гледам има
много по-амбициозна цел (генерализирани overlay-и) и прави нещо много
по-сложно от това, което обяснявам по-горе.  При всички положения
state-ът, който dovtk-lasso има нужда да пази, е много по-сериозен от
твоята селекция.

> > Вярно е, че понякога тригерирането на пълен redraw при всяко
> > събитие
> > може да води до големи разходи на ресурси.
> 
> Дизайнът на GTK 3 е изцяло подчинен на пестене на
> ресурси.  Резултатът
> е много повече код (като обем, а и понякога като сложност) за
> разработчиците на приложения, но за сметка на повече ресурси.  Както
> се казваше навремето: казармата е тежка, но за сметка на това
> продължителна.
> 
> Няма страшно, идва GTK 4, което ще е супер-юбер-хипер версия на
> библиотеката.  Толкова гот, че планират да чупят ABI регулярно.

Безумията с GTK 4 настрана, аз имам идея как да се намали разхода на
изчислителни ресурси при решението с GTK 3, но нарочно не го споделям,
защото не искам да разводнявам дискусията преди да съм уверен, че си
разбрал базовото решение, което ти предлагам.

> > Струва ми се, че проблемите които срещаш с портването на селекцията
> > са следствие от това, че авторът поначало е организирал рисуването
> > нехигиенично.
> 
> Не, това е генерален проблем при GTK и cairo, който не съществува при
> GDK 2.  Както е писал потребителя във връзката, която дадох.

Не съм съгласен с тази оценка. Поне от това, което виждам дотук, GTK 2
е бил използван неправилно.

Attachment: 1-selection-active.png
Description: PNG image

Attachment: 2-window-maximized.png
Description: PNG image

Attachment: 3-window-restored.png
Description: PNG image

Attachment: 4-selection-canceled.png
Description: PNG image


reply via email to

[Prev in Thread] Current Thread [Next in Thread]