arca-gui migrates from egui to GPUI and redesigns the window with GPUI Kit - #1
Open
Villoh wants to merge 43 commits into
Open
arca-gui migrates from egui to GPUI and redesigns the window with GPUI Kit#1Villoh wants to merge 43 commits into
Villoh wants to merge 43 commits into
Conversation
La rama anterior se hizo sobre c4c0354 y main se movio 18 commits por debajo: renombrar dentro del archivo, el visor, la vista plana, los grupos por mascara, las columnas y el modo oscuro en blanco y negro. Los dos lados reescribieron main.rs desde la misma base, asi que el merge salieron 46 hunks que son el mismo conflicto repetido: la rama renombro todos los accesos al estado y main escribio mil seiscientas lineas nuevas contra la forma vieja. Resolver eso no es elegir un lado, es rehacer la extraccion encima del main de hoy, y se hace asi a proposito: el compilador comprueba la transformacion. Un hunk mal resuelto compila; un self.archive que se escape de la reescritura, no. Este commit trae solo lo que no toca main.rs y por tanto no choca: el spike, los planes, Cargo.lock fuera de .gitignore y las dependencias de GPUI Kit. gpui_shell.rs y gpui_theme.rs vienen en el arbol pero main.rs todavia no los declara, asi que no se compilan: la ventana GPUI se enciende en el commit siguiente, cuando exista AppController. El analisis de esa extraccion --que campo va donde, cuales de los 57 metodos se mueven, y el unico sitio que no es mecanico-- esta en docs/plans/portar-controlador-a-main.md para que no viva en la cabeza de nadie. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Faltaba lo que una sesion nueva no puede deducir del codigo: en que rama esta cada cosa, y sobre todo que la rama vieja no se borra porque su main.rs es la implementacion de referencia de AppState, AppController y AppAction. Rehacerlos desde cero seria trabajo tirado. Tambien la lista de comprobacion manual. cargo test no toca la UI de egui, asi que si la extraccion se come el visor o los grupos por mascara, la suite pasa verde igual y nadie se entera. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Villoh
force-pushed
the
gpui-kit-redesign
branch
from
September 8, 2026 19:48
c96f88f to
53bf201
Compare
# Conflicts: # arca-gui/Cargo.toml # arca-gui/src/main.rs
El shell GPUI llevaba 149 de los 161 textos en ingles fijo dentro del codigo: en espanol se veia media ventana sin traducir y un lector de pantalla leia los nombres de las regiones en ingles. Ahora sale todo de i18n.rs. Diecisiete cadenas nuevas, las que GPUI necesita y egui no tenia: los nombres de las regiones que solo existen aqui -- barra de acciones, estado, progreso, contenido -- y los estados vacios, que en egui eran una sola frase y aqui distinguen archivo sin entradas, carpeta vacia y filtro sin coincidencias.
Idioma, tema, y el formato, compresor, nivel y subcarpeta con que se crea un archivo nuevo. Hasta ahora la superficie GPUI leia gui.conf al arrancar y no habia forma de tocarlo sin editar el fichero a mano. Cada preferencia es una fila de botones con el actual relleno, no un desplegable: hay tres como mucho de cada, y una lista asi de corta cuesta mas abrirla que leerla. El teclado y el raton pasan por settings_activate, que es una sola funcion, para que Enter y el clic no puedan acabar haciendo cosas distintas.
Solo habia Ctrl+F, C, X y V. Ahora tambien Ctrl+O, Ctrl+N, Ctrl+E, Ctrl+T, Ctrl+I, Ctrl+Shift+C, Alt+W, F5, F1 y Escape, y una ventana F1 que los lista. Un solo manejador en la raiz en vez de una docena de acciones GPUI con sus bindings: cada atajo es la misma forma -- una tecla, una guarda y una accion ya escrita -- y una tabla de ellos se lee de una vez. La lectura de la tecla sale a shortcut_for, que no toca estado, para poder comprobar sin ventana que Ctrl+O sigue valiendo con el cursor dentro del filtro y que F5 no.
La superficie GPUI no tenia menu contextual: abrir, extraer la seleccion, extraer aqui, probar la seleccion, quitar, copiar, cortar, pegar, copiar nombres y seleccionar todo solo estaban en la barra, en el menu de overflow o en una tecla que habia que saberse. Cada entrada nombra su atajo, para que el menu se deje de usar solo. Copiar, cortar y pegar se quedan fuera donde el escritorio no tiene portapapeles de ficheros, en vez de aparecer en gris: una entrada que no puede hacer nada nunca es peor que no tenerla. Pulsar donde no hay menu lo cierra; el menu esta occluded, asi que sus propios clics no llegan al fondo.
El ancho estaba clavado en el codigo, uno por columna, y no se guardaba. Ahora sale de Settings::widths, el mismo campo de gui.conf que llevaba usando la ventana egui, asi que una anchura puesta en una superficie aparece en la otra. El tirador va absoluto dentro de la celda de cabecera, no como elemento de la fila: un separador con ancho propio correria cada cabecera unos pixeles respecto a la columna que nombra. El raton se sigue en la ventana y no en la tira, porque el puntero se sale de seis pixeles enseguida. Doble clic ajusta la columna a lo que lleva dentro. El nombre sigue siendo elastico en vez de tener ancho fijo: es lo que hace que la lista llegue al canto derecho.
Todo el archivo de una vez, sin carpetas, que es como se busca algo cuando no se sabe donde esta. El controlador ya lo tenia en visible_rows leyendo settings.flat; lo que faltaba era la forma de encenderlo. Al encenderla se enciende la columna Ruta si estaba apagada, porque una lista sin carpetas encima tiene que decir de donde sale cada nombre.
Faltaba todo lo que escribe: anadir ficheros, nueva carpeta, renombrar, deshacer, guardar una copia, contrasena por defecto y seleccionar por mascara. La superficie GPUI solo sabia abrir, extraer y borrar. undo_last, drop_undo y reread_names se van de la vista egui al AppController. No tienen nada de un toolkit dentro y las dos superficies las ofrecen; tenerlas colgando de ArcaApp era la razon por la que GPUI no podia deshacer. Los cuatro dialogos que piden un texto comparten un solo campo con el nombre que le pone el dialogo abierto, porque son modales y excluyentes entre si: un campo con tres nombres es un campo, no tres que estan vacios casi siempre. El mismo motivo por el que el volcado de lo tecleado sale a push_to_owner: habia dos copias identicas y un campo nuevo se olvidaba en una de ellas.
Los recientes cuelgan del menu, diez como mucho, por ruta y no por nombre para que dos que se llaman igual se distingan, con su escoba al final. La pagina de codigo se va a los ajustes en vez de a un submenu: es una preferencia guardada en gui.conf, y ese es el sitio de las preferencias guardadas. Cambiarla relee los nombres del archivo abierto en el sitio, sin volver a abrirlo.
F3, o el menu de la fila, abre lo que hay dentro del archivo sin sacarlo: como texto, como hex o como la imagen que es. El controlador ya sabia leerlo -- view_entry estaba en AppController -- y GPUI no tenia donde ponerlo. El texto y el hex van por uniform_list, asi que solo se maquetan las lineas en pantalla y un log de un millon de lineas se abre igual de rapido que una nota de tres. La imagen se decodifica una vez y se guarda con el nombre del que salio: darle a GPUI una Image nueva por fotograma seria decodificar una foto de treinta megas sesenta veces por segundo. La pestana de imagen solo aparece cuando hay imagen, y la fila de una carpeta no ofrece visor: una pestana que dice imagen sobre un fichero de texto es una pestana que miente.
El merge f10f5b4 se quedo con el main.rs de la rama tal cual y con el i18n.rs de main tal cual, asi que la ventana egui perdio features que si estaban en 0.6.6. moving_word, que nadie usaba en ninguna de las dos superficies, era la senal. Vuelven Job::Move, moved_name con su prueba de las dos escrituras de barra, y move_into, ahora en AppController y no colgando de la vista egui. Con ellos vuelve tambien el apunte de deshacer en run_job: sin el, state.undo no se escribia nunca y Ctrl+Z y su entrada de menu estaban apagados para siempre. En GPUI el gesto es el mismo que en egui: mientras la seleccion esta en el aire no se decide nada. Si cae sobre una carpeta del archivo, se mueve ahi; si sale de la lista, la coge arca-drag. El arrastre nativo no puede empezar antes, porque en cuanto empieza el sistema se queda el puntero y ya no hay vuelta a la lista.
Lo otro que se llevo por delante el merge f10f5b4. arca-net seguia en Cargo.toml sin que nadie lo llamase, y update_ready, check_updates, update_tampered y update_installing estaban en i18n.rs sin usar: esa era la senal. Vuelve el aviso entero -- preguntar una vez al arrancar, en un hilo y sin decir nada; la entrada arriba del menu con un punto en el boton para que se vea desde fuera; bajar el instalador comprobando el SHA-256 contra las sumas publicadas al lado; y dejar que Inno cierre y reabra Arca. Una copia sacada del .zip no se actualiza sola, porque son ficheros sueltos en una carpeta que eligio alguien: a esa se le ofrece la pagina. Solo se aceptan direcciones https de nuestro propio repositorio. Y con ello Settings::save, que construia las lineas de flat, tree, page, window y recent en un String que luego tiraba. Seis ajustes se leian al arrancar y no se escribian nunca: por eso la lista de recientes salia siempre vacia. El texto del fichero sale ahora a Settings::text y la prueba lo lee de vuelta sin tocar disco, que es la unica forma de que no se vuelva a romper en silencio.
La tercera que se llevo el merge, y con ella lo que nadie usaba en i18n.rs: elapsed_word, time_left, pause_word, resume_word, paused_word y stopping. Con ella vuelve tambien Cancelar, que estaba muerto: AppAction::CancelJob era un brazo vacio y el notify del worker devolvia true siempre, asi que el boton no paraba nada. Ahora hay dos banderas -- parar y esperar -- que el worker lee al acabar cada entrada, que es el unico momento en que no esta en mitad de algo. Pausar suelta al final de una entrada, no de un byte: un fichero empezado tiene que terminar. Parar pasa aunque este en pausa, para no tener que reanudar algo solo para poder abandonarlo. Las banderas se renuevan por trabajo en vez de bajarse: un hilo al que se le dijo que parase puede seguir de salida, y no debe leer la bandera que mira el trabajo siguiente. El trabajo vuelve a distinguir si se lanzo desde la lista -- panel encima, con el fondo atenuado -- o desde el Explorador, que es la ventana entera porque no hay lista detras a la que volver. En GPUI la tira de progreso gana el reloj, la estimacion y el boton de pausa.
Un trabajo abandonado decia en rojo lo que dijese el error de dentro. No hay nada mal en el archivo: la reescritura se rindio antes de cambiar nada, y eso es lo que dice ahora, en el color de lo que solo es una noticia. Era la ultima cadena de i18n.rs que no usaba nadie.
Los dos gestos de puntero que faltaban. La lista de GPUI es virtualizada, asi que sobre que fila esta el raton es aritmetica con el desplazamiento y no un rectangulo que alguien haya guardado: eso sale a row_under, que se comprueba sin ventana porque una fila de diferencia ahi seleccionaria todo corrido un sitio. La goma sigue la regla del Explorador, que es la unica que deja los dos gestos en el mismo boton: pulsar sobre algo ya marcado y tirar lo lleva a otro sitio, pulsar en cualquier otro lado y tirar dibuja una seleccion nueva. Ctrl suma a lo que habia. Pasado un borde la lista corre detras del puntero, porque si no una seleccion nunca podria ser mas larga que la ventana. La rueda pulsada deja un ancla y la lista corre hacia el puntero, mas rapido cuanto mas lejos, con wheel_speed, que es la misma funcion y las mismas pruebas que usa egui. La suelta cualquier otro boton, la propia rueda girando o Escape. Dos diferencias con egui, a proposito: el paso lo da el tick de 100 ms que ya tenia el shell en vez de un fotograma, y el ancla es un anillo sin las flechas que Windows dibuja sobre el puntero, porque eso pedia esconder el cursor del sistema.
Se veia OpenEnter, ViewF3, RemoveSupr. Las entradas se construian con una tabulacion dentro del texto, que es como egui dibuja una columna a la derecha; GPUI maqueta texto y ahi una tabulacion no es nada, asi que el atajo quedaba soldado a la palabra. El atajo pasa a ser un hijo aparte, alineado a la derecha y en la tinta apagada, porque es una forma de llegar y no una segunda cosa que leer. Como ya no hay que meterlo en la cadena, lo tienen todas las entradas que lo tengan y no solo las tres del portapapeles: probar, seleccionar todo, invertir, soltar, deshacer y contrasena por defecto tambien lo dicen ahora. Y vuelven las rayas que agrupaban ese menu en egui: mirar, cambiar, mover por el portapapeles y trabajar sobre la seleccion son cuatro cosas distintas, y doce entradas seguidas son un muro.
La leyenda nativa se queda transparente y la dibuja TitleBar de GPUI Kit, con los mismos tokens que todo lo que hay debajo: el arrastre, el doble clic para maximizar y los tres botones de la ventana los lleva ella. Dentro va el nombre de lo que esta abierto, que es para lo que sirve una barra de titulo. Nada pulsable ahi dentro a proposito: la barra es la zona de arrastre, y un control dentro de ella movería la ventana en cuanto la mano temblase de camino a pulsarlo. Dos cosas que el alto nuevo movia. El menu de overflow flota en coordenadas de ventana, asi que ahora cuenta la barra o se dibujaba encima de la fila de botones en vez de debajo. Y los dos velos que tapan el fondo -- los dialogos y el selector de ficheros del sistema -- empiezan bajo la barra en vez de cubrirla: los tres botones de la ventana no son fondo, y un dialogo que los tapase seria un dialogo que no deja cerrar la ventana que hay detras.
… ventana Dos fallos distintos con la misma cara. La goma leia el alto de fila de last_item_size.item, que pese al nombre es el tamano del viewport y no el de una fila: dividia por cuatrocientos en vez de por veintiocho, asi que todo punto caia en la primera fila y no se marcaba nada. El alto sale ahora del contenido entre el numero de filas, que es de donde GPUI lo saco, y la division va a row_height con su prueba para que el nombre no vuelva a enganar a nadie. Y arrastrar fuera no hacia nada porque AppController::drag_out era un cuerpo vacio: la implementacion de Windows con arca-drag tambien se la comio el merge f10f5b4, y arca-drag llevaba desde entonces en Cargo.toml sin que la llamase nadie. Vuelve entera, sin extraer nada al empezar: al shell se le da una lista de nombres y tamanos y va pidiendo un fichero cada vez mientras suelta, asi que un arrastre del que uno se arrepiente no cuesta nada. El disparo pasa de la fila a la ventana. on_drag_move se dispara con cualquier movimiento, y comparando contra el rectangulo de la fila el arrastre nativo empezaba en cuanto el puntero llegaba a la fila de al lado -- que no es salir de la lista, y ademas hacia imposible soltar sobre una carpeta. Ahora se mira una sola vez, contra la lista entera.
Dos costes, los dos por movimiento del raton, que es a cien hercios. visible_rows ordenaba llamando a to_lowercase() dentro del comparador: dos String por comparacion, o sea unas treinta mil asignaciones por lista de mil quinientas filas. Ahora se pliega caracter a caracter con Iterator::cmp, sin asignar nada. Eso lo paga todo el que ordene, tambien el repintado normal. Y la lista se construia entera dos o tres veces por evento. La goma se la queda congelada al empezar el gesto -- lo que marca no puede cambiar de donde lo marca, y con un boton bajado no hay nada mas que la cambie -- y lo unico que las demas necesitaban de ella era cuantas filas hay, que ahora lo apunta el repintado, que es el que ya lo sabia.
No habia perfil dev, asi que cargo run compilaba todo a opt-level 0: GPUI entero, su motor de layout, el modelador de texto y el rasterizador, que rehacen su trabajo en cada fotograma. Eso no es un poco mas lento, es la diferencia entre una lista que sigue al puntero y una que va a tirones al arrastrar y al marcar. Las dependencias pasan a 3 tambien en depuracion: no son codigo de Arca y nadie las depura. El codigo de Arca a 1, que es lo que necesitan ordenar la lista y recorrer las entradas para no notarse, y a ese nivel el depurador sigue parando donde se le dice. La primera compilacion despues de esto rehace el arbol entero -- seis minutos y medio aqui -- y a partir de ahi un cambio en arca-gui vuelve a ser ocho segundos.
Villoh
marked this pull request as ready for review
September 11, 2026 21:49
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Migrate
arca-guifrom egui/eframe to GPUI and redesign the GPUI surface on top ofGPUI Kit. This covers phases 1 through 7 of the plan
added in
docs/plans/migration-to-gpui.md.Intentionally a draft: the GPUI window is already standing and looks good,
but there are still things to polish and platform validation to complete. The
list is below, under “What remains”.
Why
arca-guiwas a ~4,600-line window insrc/main.rswith business logic andegui rendering interleaved. The goal is not just to change toolkits, but to
keep state out of the type that the toolkit renders.
What changes, layer by layer
1. State is separated from the toolkit
An
AppControllernow owns the inputs, current folder, history, selection,filter, sort order, jobs, and pending dialogs. It is driven through
AppActioninstead ofegui::Context.The workers and message channel are unchanged: no disk or compression
operation touches the UI thread. That is precisely what allows two possible
windows to share the same logic, and what makes this PR incremental rather
than a big bang.
2. GPUI comes from GPUI Kit, not a Zed revision
The original plan pinned
zed-industries/zed@3384317. That has been removed:gpui-componentis built againstgpui-pre ^0.3.which do not link together.
The solution is to rename the packages in
Cargo.toml, so not a singleusein the code changes:Resulting unified graph:
gpui-pre 0.3.4+gpui-base 0.6.0+gpui-component 0.6.0. Reproducibility comes fromCargo.lock, which is nolonger in
.gitignore— a pin that is not versioned is not a pin.spikes/gpuikeeps the original revision: it records what was validated inG1 and is not rewritten retroactively.
3. Monochrome light and dark theme (
arca-gui/src/gpui_theme.rs)Six grays per mode, hue 225 (the night blue from
brand/BRAND.md) at 8–12%saturation, inverted between light and dark.
background#0E1014#FAFAFBsurface#14161B#FFFFFFraised#1B1E25#F3F4F6border#262A33#E3E5EAtext#E4E6EB#14161Bmuted#8B909C#666B76There is no accent color. The selected row, keyboard cursor, and focus
ring use the text color at different strengths (12%, 100%, 70%). Contrast is
guaranteed by construction rather than by a table someone has to maintain: if
the text is readable, the selection is visible.
The only exceptions are
dangerandwarning. They are not decoration — theydistinguish “extracted” from “not extracted”, and anyone scanning before
reading needs to be able to see that distinction. They remain at the lowest
chroma that is still legible as red and amber on both backgrounds.
The tokens are the ones from
gpui-component; Arca does not add its owntoken layer, it only specifies the value of each token.
Radius 4/6 px, row height 26 px, base font size 13 px.
4. Layout redesign
The reference is Nohrs (the same problem:
a file explorer) with the density of DBFlux.
Before: a stack of floating strips in
p_3+gap_3. Now: full-bleed regionsseparated by 1 px lines, with padding inside each bar so every divider spans
the full width.
gpui_component::sidebar). It is content,not chrome: until now, reaching a deeply nested file meant double-clicking
down and navigating back up. The current folder branch opens automatically;
the rest stays closed. The tree is cached by (file path, entry count) —
rebuilding it in
rendermeant walking all names 60 times per second duringextraction.
gpui_component::status_bar), withthe summary on the left and counters on the right. The counters used to live
in the middle of the navigation row, pushing the breadcrumbs against the
right edge.
Seven outlined boxes read as seven competing things.
IconName, viagpui-kit-assets) instead of‹ › ↑.half the window down.
and alternating stripes.
5. Accessibility
What was already there is preserved and not downgraded: AccessKit roles,
visible focus, keyboard navigation, focus trapping in modals, and the
background subtree without roles while an overlay has focus. Icon buttons keep
their accessible names as words.
Verification
cargo test --workspace— green (default build, egui).cargo test -p arca-gui --features gpui— 38/38.cargo fmt -p arca-gui -- --check— clean.table, status bar, dark theme.
Two new tests in
gpui_theme.rs:both modes. With no accent, the palette is the accessibility, so this is
the test that matters.
that stops being the inverse of the other. These are copy-and-paste slips that
compile and do not show up in a diff of thirty hex literals.
I wrote a third test that failed (it assumed the three backgrounds are ordered
in the same direction in both modes). That is false: in light mode a panel is
raised toward white and a hover is tinted away from it. Rather than tuning
the threshold until it passed, I replaced it with the thing that can actually
break.
What remains
That is why this is a draft.
the ones from
gpui-component:Input— removes the roughly 400 lines ofFilterInputand its UTF-16IME contract. This is the largest pending deletion.
Modal/Root,Popover,Table,Notification.absoluteand fixed offsets (top(38.) right(232.)),rather than being anchored to their trigger. They break if the filter width
or font size changes. The correct solution is
Popover.theme and language to be changed without editing
gui.conf. For now thepreference is read at startup and
Systemfollows the desktop.and macOS are still unmarked in
docs/plans/gpui-spike-baseline.md, andthey should not be marked without a real test.
The plan says any accessibility limitation blocks removing egui, so this is
required before phase 8.
eframe,egui,egui_extras) is not done, and should not be done until 4 and 5 are closed.Notes for reviewers
multiline
dialog_buttoncalls with a bad regex. Theon_clickbodiessurvived intact, and
modal_focus_targets/modal_enterpin the exact id andfocus handle of each button, so the reconstruction is deterministic except
for the visible text of three:
"Set Password"/"Unlock","Keep Both"/"Keep Both Always", and"Delete". They deserve a look.are not split further because any intermediate cut inside
gpui_shell.rsproduces a commit that does not compile.
Settings::loadalready reads thecolumnskey, which was a parity defectnoted in G1. The
gui.confformat does not change.