From fcb2cca8441ba50b7396dbfc58925789f24688d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Sat, 2 May 2026 19:41:10 -0300 Subject: [PATCH] Fixes for position: sticky, absolute and fixed. --- bin/unit_tests/assets/html/ensoft/ensoft.css | 621 ++++++++++++++++++ bin/unit_tests/assets/html/ensoft/ensoft.html | 286 ++++++++ bin/unit_tests/assets/html/ensoft/fx.png | Bin 0 -> 6349 bytes bin/unit_tests/assets/html/ensoft/logo.png | Bin 0 -> 5696 bytes bin/unit_tests/assets/html/ensoft/rss.png | Bin 0 -> 1152 bytes include/eepp/ui/uihtmlwidget.hpp | 22 + include/eepp/ui/uirichtext.hpp | 1 + src/eepp/ui/uihtmlwidget.cpp | 245 +++++-- src/eepp/ui/uirichtext.cpp | 30 + .../unit_tests/uihtml_position_tests.cpp | 87 +++ src/tests/unit_tests/uihtml_tests.cpp | 68 ++ 11 files changed, 1302 insertions(+), 58 deletions(-) create mode 100644 bin/unit_tests/assets/html/ensoft/ensoft.css create mode 100644 bin/unit_tests/assets/html/ensoft/ensoft.html create mode 100644 bin/unit_tests/assets/html/ensoft/fx.png create mode 100644 bin/unit_tests/assets/html/ensoft/logo.png create mode 100644 bin/unit_tests/assets/html/ensoft/rss.png diff --git a/bin/unit_tests/assets/html/ensoft/ensoft.css b/bin/unit_tests/assets/html/ensoft/ensoft.css new file mode 100644 index 000000000..1db20d51f --- /dev/null +++ b/bin/unit_tests/assets/html/ensoft/ensoft.css @@ -0,0 +1,621 @@ +@import url(https://fonts.googleapis.com/css?family=Lato); +@import "../anim.css"; +a:active, +a:focus { + outline: none; +} +.no_selection { + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} +.blue_links a { + text-decoration: none; + color: #4183C4; +} +.blue_links a:hover { + text-decoration: underline; +} +body { + font-family: 'Lato', Helvetica, sans-serif; + font-size: 16px; + background-color: #ffffff; + margin: 0; + padding: 0; + overflow-x: hidden; +} +::-moz-selection { + background-color: #d00000; + color: #fff; +} +::selection { + background-color: #d00000; + color: #fff; +} +input, +button, +textarea { + font-family: inherit; +} +#bar.smallscreen { + position: absolute !important; + width: 200px !important; +} +#bar { + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + position: fixed; + top: 0px; + bottom: 0px; + width: 250px; + border-right: 1px solid #e0e0e0; +} +#bar #logo { + margin: 40px 0px 0px 50px; + height: 144px; + width: 100px; + background: url('logo.png') no-repeat center; +} +#bar #logo #logo-fx { + position: absolute; + top: -27px; + left: -14px; + height: 225px; + width: 225px; + background: url('fx.png') no-repeat center; + opacity: 0; + -ms-filter: "alpha(opacity=0)"; + -khtml-opacity: 0; + -webkit-animation-name: contract; + -moz-animation-name: contract; + -webkit-animation-duration: 6s; + -moz-animation-duration: 6s; + -webkit-animation-iteration-count: infinite; + -moz-animation-iteration-count: infinite; + -webkit-animation-timing-function: linear; + -moz-animation-timing-function: linear; +} +#bar #links li { + margin: 40px 30px 0px 0px; + list-style-type: none; +} +#bar #links li a { + padding: 10px 20px 10px 20px; + text-decoration: none; + color: #808080; +} +#bar #links li a:hover { + color: red; +} +#bar #rss { + margin-top: 100px; + margin-left: 20px; +} +#bar #rss a { + background: url("rss.png") no-repeat 8px center; + padding: 5px 10px 5px 24px; + text-decoration: none; + font-size: 12px; + border-radius: 4px; + -khtml-border-radius: 4px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + behavior: url('https://ensoft.dev/assets/js/PIE.php'); + background-color: #d0d0d0; + color: white; +} +#bar #rss a:hover { + background-color: red; +} +#content.smallscreen { + left: 200px !important; +} +#content { + position: absolute; + left: 250px; + right: 0; + top: 0; + padding: 0px; +} +#content >h1 { + margin: 50px 0 0 50px; + font-size: 35px; + font-family: 'Lato'; + font-weight: 900; + padding: 0; +} +a img { + border: 0; +} +#ajax-target { + position: relative; +} +.loading_corner { + position: fixed; + bottom: 90px; + right: 0px; + width: 32px; + height: 32px; + background: url("loading.gif?v=3") no-repeat !important; +} +.kajax-loading .kajax-loading-image { + width: 32px; + height: 32px; + background: url("loading.gif?v=3") no-repeat center white; +} +#content > .kajax-loading { + position: fixed; + top: 10px; + left: 260px; +} +.stylish-form > .kajax-loading { + position: absolute; + bottom: 45px; + right: 190px; +} +.blend_input { + display: block; + overflow: hidden; + outline: none; + resize: none; + background: transparent; + border: none; +} +.admin-editor { + width: 800px; + margin-left: 100px; + position: relative; +} +.admin-editor .title { + color: #808080; + width: 800px; + font-size: 40px; + height: 40px; + display: block; + overflow: hidden; + outline: none; + resize: none; + background: transparent; + border: none; +} +.admin-editor .body { + color: #808080; + padding-left: 60px; + margin-left: -50px; + background: url('markdown.png') no-repeat top left !important; + margin-top: 40px; + display: block; + overflow: hidden; + outline: none; + resize: none; + background: transparent; + border: none; + width: 800px; + height: 600px; + margin-bottom: 200px; + white-space: pre-wrap; + word-wrap: break-word; + font-size: inherit; +} +.square-link { + display: block; + float: left; + padding: 15px; + margin: 14px 5px 14px 0; + font-weight: bold; + text-decoration: none; + color: #ffffff; + border: none; + cursor: pointer; +} +.square-button { + display: block; + float: left; + padding: 15px; + margin: 14px 5px 14px 0; + font-weight: bold; + text-decoration: none; + color: #ffffff; + border: none; + cursor: pointer; + background-color: black; + border-radius: 5px; + -khtml-border-radius: 5px; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + behavior: url('https://ensoft.dev/assets/js/PIE.php'); + font-size: 16px; + color: white !important; +} +.square-button:hover { + background-color: red; +} +#admin-bar { + position: fixed; + bottom: 0px; + left: 0px; + height: 76px; + width: 100%; + background-color: #202020; + z-index: 2; +} +#admin-bar .container { + width: 800px; + margin: 0 auto; +} +#admin-bar .container .left { + float: left; +} +#admin-bar .container .right { + float: right; +} +#admin-bar .container span { + display: block; + float: left; + padding: 15px; + margin: 14px 5px 14px 0; + font-weight: bold; + text-decoration: none; + color: #ffffff; + border: none; + cursor: pointer; +} +#admin-bar .container a, +#admin-bar .container input[type=submit] { + display: block; + float: left; + padding: 15px; + margin: 14px 5px 14px 0; + font-weight: bold; + text-decoration: none; + color: #ffffff; + border: none; + cursor: pointer; + background-color: black; + border-radius: 5px; + -khtml-border-radius: 5px; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + behavior: url('https://ensoft.dev/assets/js/PIE.php'); + font-size: 16px; + color: white !important; +} +#admin-bar .container a:hover, +#admin-bar .container input[type=submit]:hover { + background-color: red; +} +#admin-posts { + width: 900px; + margin: 0 auto; +} +#admin-posts h1 { + float: left; +} +#admin-posts .button { + float: right !important; + padding: 10px; + font-size: 12px; + margin-top: 26px; +} +#admin-posts ul { + padding: 0; + margin: 0; + clear: both; +} +#admin-posts li { + position: relative; + list-style-type: none; + padding: 18px 0 20px 0; + border-bottom: 1px solid #e0e0e0; +} +#admin-posts li a { + color: inherit; + font-weight: bold; + text-decoration: none; +} +#admin-posts li a:hover { + color: red; +} +#admin-posts li span { + float: right; + display: none; +} +#admin-posts li span a { + color: #a0a0a0; + margin-left: 10px; + font-size: 12px; +} +#admin-posts li em { + position: absolute; + right: 0px; + bottom: 0px; + font-size: 8px; + color: #a0a0a0; +} +#admin-posts .post_block { + float: left; + width: 400px; + padding: 20px; +} +#admin-posts #drafts { + float: left; + width: 400px; + padding: 20px; + border: 4px solid black; + border-top: none; +} +#admin-posts #published { + float: left; + width: 400px; + padding: 20px; + color: #a0a0a0; +} +#admin-posts #logout { + font-weight: bold; + float: right; + clear: both; + font-size: 14px; + margin: 5px 36px -50px 0; +} +#admin-posts #logout a { + text-decoration: none; + color: #4183C4; +} +#admin-posts #logout a:hover { + text-decoration: underline; +} +#admin-posts #logout a { + color: #505050 !important; +} +.big_title { + margin: 50px 0 0 50px; + font-size: 35px; + font-family: 'Lato'; + font-weight: 900; + padding: 0; +} +.blog_post { + border-bottom: 1px solid #e0e0e0; +} +.blog_post .date { + border-bottom: 1px solid #e0e0e0; + padding: 25px 0 25px 50px; + font-size: 12px; + font-weight: bold; +} +.blog_post >h1 { + margin: 50px 0 0 50px; + font-size: 35px; + font-family: 'Lato'; + font-weight: 900; + padding: 0; +} +.blog_post >h1 >a { + color: black; + text-decoration: none; +} +.blog_post >h1 >a:hover { + color: red; +} +.blog_post .markdown { + width: 500px; + margin: 30px 0 65px 50px; + color: #808080; +} +.blog_post .markdown h1, +.blog_post .markdown h2, +.blog_post .markdown h3, +.blog_post .markdown h4, +.blog_post .markdown h5 { + color: black; +} +.blog_post .markdown li { + line-height: 2.0em; +} +.blog_post .markdown a { + text-decoration: none; + color: #4183C4; +} +.blog_post .markdown a:hover { + text-decoration: underline; +} +.blog_post .markdown img { + border: 5px solid #f0f0f0; + padding: 10px; + border-radius: 10px; + -khtml-border-radius: 10px; + -webkit-border-radius: 10px; + -moz-border-radius: 10px; + behavior: url('https://ensoft.dev/assets/js/PIE.php'); +} +.blog_post .markdown blockquote { + border-left: 5px solid #505050; + padding-left: 16px; + margin: 0; +} +.blog_post .markdown .footnotes { + margin-top: 50px; + font-size: 14px; +} +.blog_post .markdown .footnotes hr { + color: #e0e0e0; + background-color: #e0e0e0; + border: none; + height: 1px; +} +.padded { + padding: 50px 0 0 50px; +} +.styled-box { + border: 5px solid black; + width: 440px; + margin: 50px 0 0 50px; + padding: 0px; +} +.stylish-form { + position: relative; + width: 100%; + height: 100%; + margin: 0; + padding: 0; +} +.stylish-form .mail_icon { + background: url('mail.png') no-repeat center right; +} +.stylish-form .robot_icon { + background: url('robot.png') no-repeat center right; +} +.stylish-form p { + color: #808080; + font-size: 18px; +} +.stylish-form li { + list-style-type: none; + border-bottom: 1px solid #e0e0e0; + padding: 20px; + margin: 0; +} +.stylish-form input[type=password], +.stylish-form input[type=text], +.stylish-form textarea { + font-size: 22px; + width: 100%; + display: block; + overflow: hidden; + outline: none; + resize: none; + background: transparent; + border: none; + padding-left: 10px; +} +.stylish-form input[type=password]:focus, +.stylish-form input[type=text]:focus, +.stylish-form textarea:focus { + border-left: 4px solid red; +} +.stylish-form input[type=submit] { + display: block; + float: left; + padding: 15px; + margin: 14px 5px 14px 0; + font-weight: bold; + text-decoration: none; + color: #ffffff; + border: none; + cursor: pointer; + background-color: black; + border-radius: 5px; + -khtml-border-radius: 5px; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + behavior: url('https://ensoft.dev/assets/js/PIE.php'); + font-size: 16px; + color: white !important; + float: none; +} +.stylish-form input[type=submit]:hover { + background-color: red; +} +#check-mail-target { + display: none; + text-align: right; + font-style: italic; + margin: 0; +} +#check-mail-target a { + text-decoration: none; + color: #4183C4; +} +#check-mail-target a:hover { + text-decoration: underline; +} +.placeholder { + color: #808080; +} +.form-error { + padding: 5px !important; + display: none; + background-color: #ffffb4; +} +.form-error p { + text-align: center; + display: none; +} +.form-error p:first-child { + display: block !important; +} +#save-success-msg { + position: fixed; + bottom: 0px; + right: 10px; + height: 96px; + width: 100px; + text-align: center; + display: none; + padding: 5px !important; + color: #FFFFFF; + font-weight: bold; + background-color: red; + border-radius: 5px; + -khtml-border-radius: 5px; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + behavior: url('https://ensoft.dev/assets/js/PIE.php'); +} +#minimap { + display: none; + opacity: 0.5; + position: fixed; + top: 0px; + bottom: 0px; + right: 0px; + width: 120px; + list-style-type: none; + padding: 0; + padding: 20px 0 20px 0; + margin: 0; + border-right: 1px solid #e0e0e0; +} +#minimap li .mark { + position: absolute; + bottom: 0; + top: 0; + right: 0; + width: 10px; + background-color: #c00000; + display: none; +} +#minimap li .selected { + background-color: #606060 !important; +} +#minimap li a { + position: relative; + margin-top: 5px; + background-color: #b0b0b0; + border-right: 0; + display: block; + padding: 5px; + padding-right: 10px; + text-align: center; + color: #a0a0a0; + text-decoration: none; + font-size: 12px; + font-weight: bold; + color: white; + -moz-border-radius-topleft: 4px; + -moz-border-radius-bottomleft: 4px; + -webkit-border-top-left-radius: 4px; + -webkit-border-bottom-left-radius: 4px; + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; +} +#minimap li a:hover { + background-color: #808080; +} diff --git a/bin/unit_tests/assets/html/ensoft/ensoft.html b/bin/unit_tests/assets/html/ensoft/ensoft.html new file mode 100644 index 000000000..8756af872 --- /dev/null +++ b/bin/unit_tests/assets/html/ensoft/ensoft.html @@ -0,0 +1,286 @@ + + + ensoft + + + + + + + + + + +
+ +
+ +

+ + Pampa Energía en Tecnópolis +

+

Tuvimos el placer de desarrollar en conjunto con el equipo de Red Katana uno de los juegos principales para el stand de Pampa Energía en su stand de Tecnópolis.

+ +

El slogan de este año es "Energía para Transformar" y justamente este + es el punto central de este juego, concientizar a los jugadores acerca +de la importancia de ahorrar energía.

+ +

+ +

El juego consiste de varias habitaciones de una casa, donde los +jugadores deben encontrar de que manera los personajes están +desperdiciando la energía y ayudarlos a ahorrarla.

+ +

+ +

Nuestra labor consistió en el desarrollo completo del juego, para ser utilizado en las pantallas táctiles de dicho stand.

+ +

Stand de pampa energía en tecnópolis:

+ +

+ +

Visitantes jugando:

+ +

+
+
+ + +
+ +

+ + Tia Maria: Get Behind The Mask +

+

Bajo encargo del estudio de diseño Four Players Studio desarrollamos para la renombrada marca Tia Maria una aplicación para celulares y tablets Android que se utilizaría para presentar la nueva campaña publicitaria de la marca:

+ +

+ +

El software consiste en una serie de videos publicitarios por un lado + y por el otro la funcionalidad de escanear código QR, dentro de los +cuales reconoce los códigos ganadores e informa los premios a los +participantes.

+ +

+
+
+ + +
+ +

+ + Entropia Engine++ +

+

Características

+ +

Entropia Engine++, + es un motor para el desarrollo de videojuegos 2D multiplataforma, +creado para facilitar la creación y distribución de los mismos, pensado +para un rendimiento óptimo en cualquier plataforma en la que sea +utilizado.

+ +

Portabilidad

+ +

El código del mismo puede ser compilado para una cantidad sustancial +de plataformas, de diferente naturaleza sin modificar el código. Por +ejemplo el mismo juego, puede ser compilado para Windows, Linux, Mac OS, + iOS (iPhone y iPad) y Android (Tablets y SmartPhones).

+ +

Diferencias

+ +

Se diferencia principalmente de otros motores de desarrollo de +videojuegos 2D comerciales y código abierto es en la facilidad de +portabilidad del mismo gracias a su modularidad y extensibilidad, provee + integramente más módulos de los que ofrecen otras alternativas, entre +ellos el módulo de interfaz gráfica, que reduce los tiempos de +desarrollo en gran medida. Por otro lado, nuestra elección fue trabajar +con un lenguaje compilado y nativo para las plataformas, para poder +aprovechar al máximo el hardware de cada una de estas, pudiendo tener +control total del manejo de la memoria, y teniendo un rendimiento óptimo + en todas las plataformas.

+ +

Desarrollo

+ +

El motor se encuentra en activo desarrollo y mantenido por uno de los + miembros del equipo, se planifica expandir el mismo de acuerdo a las +necesidades que surjan para el desarrollo del videojuego ( entre ellas, +completar el portado del mismo a las plataformas móbiles, módulo de +networking, módulo de scripting ).

+ +

Licencia

+ +

El motor es de código abierto, bajo la licencia MIT1, lo que permite utilizarlo libremente tanto para aplicaciones open source como para aplicaciones comerciales.

+ +

Imágenes

+ +

Demo de Interfaz Gráfica +Demo de Interfaz Gráfica

+ +

Editor de Mapas +Editor de Mapas

+ +

Corriendo en HaikuOS2 +Corriendo en HaikuOS 

+ +

Links

+ +

Repositório en Google Code

+ +

Especificaciones Técnicas

+ +
+
+
    + +
  1. +

    Licencia MIT en Wikipedia 

    +
  2. + +
  3. +

    HaikuOS 

    +
  4. + +
+
+
+
+ + +
+ +

+ + Red Domo +

+

Tuvimos el placer de trabajar con la gente de Deitres, + una empresa Marplatense de alcance nacional que utiliza tecnología +inalámbrica de vanguardia en sístemas de monitoreo de alarmas.

+ +

El trabajo realizado consiste en un ambicioso proyecto que permite +que su sistema de alarmas esté completamente integrado y pueda ser +accedido vía web, utilizando un navegador web estándar.

+ +

Fué todo un desafío para nosotros debido a la infraestuctura que ya +tenía en funcionamiento la empresa y las características de seguridad +que requiere un sistema con estas características.

+ +

Después de muchas reuniones, y de un planeamiento cuidadoso comenzamos a desarrollar lo que hoy es DOMO.

+ +

Domo

+ +

Naturaleza del Proyecto

+ +

El proyecto fué dividido en tres partes1:

+ +

Receptora

+ +

La receptora es la encargarda de recibir todos los datos que llegan de los diferentes sistemas de alarmas. +El desarrollo se llevó a cabo usando C# y partiendo de base de una librería interna proveída por el departamento técnico de Deitres. +Fué planeada con tolerancia a fallos y problemas de conexión. +Ésta luego de procesar los paquetes los envia por un canal seguro hacia el servidor.

+ +

Servidor

+ +

El servidor fué desarrollado integramente en PHP, utilizando un Servidor Dedicado. +Su principal función es la de recibir los datos que envía la receptora, +clasificarlos de forma correspondiente y asociarlos a las cuentas de +usuario y diferentes proveedoras de alarmas distribuidas por todo el +país.

+ +

Página Web.

+ +

La página Web, a su vez está dividida en tres partes:

+ +
    +
  • Cuenta de Administrador.
  • +
  • Cuentas para proveedores de servicios de alarmas.
  • +
  • Cuentas para usuarios finales.
  • +
+ +

Fué desarrollada2 utilizando PHP y Javascript, haciendo uso intensivo de el servicio de Google Maps para visualizar en tiempo real el estado de todos los equipos de alarmas conectados al sistema.

+ +

Toda las transacciones entre el navegador y el servidor son realizadas a través de una conexión segura utilizando el protocolo SSL..

+ +

Funcionalidad

+ +

El sistema completo provee una integración total de los servicios ya +ofrecidos por la empresa, mostrando gráficos en tiempo real del +rendimiento de los equipos. Ubicando en un mapa todos los puntos donde +se encuentran las alarmas y permitiendo administrar las cuentas de +usuario y las alertas recibidas.

+ +

Domo

+ +

Además para el usuario final le permite acceder, cualquiera sea su +ubicación al estado de las diferentes alarmas que posee intaladas, +permitiendole ver el estado y los mensajes que ha enviado la alarma al +sistema.

+ +

Cabe destacar también que los usuarios que tienen contratado el servicio de domótica + pueden controlar los diferentes dispositivos conectados a las alarmas, +por ejemplo, pueden abrir y cerrar puertas, encender luces controlar +calderas, etc. Todo desde su navegador web.

+ +

Palabras Finales

+ +

La implementación principal del proyecto llevó alrededor de cuatro meses de trabajo con colaboración constante entre las partes.

+ +

Hemos formado una sólida relación de negocios con dicha empresa y al +día de hoy continuamos trabajando en agregar funcionalidad al sistema, y + siempre dispuestos a innovar de forma conjunta.

+ +
+
+
    + +
  1. +

    Debido a la discreción requerida en este tipo de proyectos, se mencionan superficialmente las características del mismo. 

    +
  2. + +
  3. +

    El diseño gráfico de la misma fué contratado externamente por Deitres y es una realización de Chic Creative

    +
  4. + +
+
+
+
+ + +
+ +

+ + Todo Deportes +

+

La web de Todo Deportes fué diseñada para una empresa que comercializa insumos para deportes extremos.

+ +

Se mantuvo un estilo simple, en tonos amarillos y negros acorde al logo de la empresa.

+ +

El sitio posee una galería interactiva en la parte superior de la +página, una navegación fluida y cuatro secciones: la página de inicio, +las marcas ofrecidas, el team sponsoreado y un formulario de contacto.

+ +

Todo Deportes

+
+
+ +
+
+ + + + + + + +
+ + diff --git a/bin/unit_tests/assets/html/ensoft/fx.png b/bin/unit_tests/assets/html/ensoft/fx.png new file mode 100644 index 0000000000000000000000000000000000000000..064b9295cb429ef78a0ba94cde928da5f88a7028 GIT binary patch literal 6349 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 TJ+?Z(|LGd)nPFxX? literal 0 HcmV?d00001 diff --git a/bin/unit_tests/assets/html/ensoft/rss.png b/bin/unit_tests/assets/html/ensoft/rss.png new file mode 100644 index 0000000000000000000000000000000000000000..f195a1ce2b1bfdbd8142e79967ee7277d3d6825f GIT binary patch literal 1152 zcmeAS@N?(olHy`uVBq!ia0vp^JRr=$1|-8uW1a&k$r9IylHmNblJdl&R0hYC{G?O` z&)mfH)S%SFl*+=BsWuD@%nF$y5hW46K32*3xq68pHF_1f1wh>l3^w)^1&PVosU-?Y zsp*+{wo31J?^jaDOtDo8H}y5}EpSfF$n>ZxN)4{^3rViZPPR-@vbR&PsjvbXkegbP zs8ErclUHn2VXFi-*9yo63F|8ji-ohRqpPWlrKzKV zld}a(uSMv>2~2MaLaz}{y`Y4UTL84#CABECEH%ZgC_h&L>|v`+oNjRex(AnA zAbL}9yTt&fUVWfr^g+>z6wNRpVA=sO;fWT=fhYUaJYZTc0w(G?QtO+58RDC#i(^Q| zt&)qj-c5lL$38BeG$Fc~MM$ZsxY6l>`v>POJ9ew}kLPvM=;@La2123GuqQa%S z^@&16g2=K70?Yq1PcP6ed+_~0?fl}8bEZ%DzVH2<&lQK1-x-$t=VdD1Z)bPmfa0BH z4}zUoum3PQXy~_%^_xJja--EUuD_3Rj0>8z=giK1VrZ~%5pT`x+&zzjCZBxZbY=d7 z3{eYLc4grl<)-A#d&LfYbI>&LU9sw4>(0F~7dn(yA8z-_SXL)9|JBsF?`k^JWfJ7| zqXMqS-&(ah@!q-y*^~*#rK_s0>m*eK%;jS;_1XF&Ot?Hkp(}RJs((L)6BryhdiHWZ S++PAJ6Fgo0T-G@yGywqYNRB%I literal 0 HcmV?d00001 diff --git a/include/eepp/ui/uihtmlwidget.hpp b/include/eepp/ui/uihtmlwidget.hpp index b2fcd64e3..1eb7d4381 100644 --- a/include/eepp/ui/uihtmlwidget.hpp +++ b/include/eepp/ui/uihtmlwidget.hpp @@ -31,21 +31,27 @@ class EE_API UIHTMLWidget : public UILayout { virtual void onDisplayChange(); CSSDisplay getDisplay() const { return mDisplay; } + void setDisplay( CSSDisplay display ); CSSPosition getCSSPosition() const { return mPosition; } + void setCSSPosition( CSSPosition position ); CSSFloat getCSSFloat() const { return mFloat; } + void setCSSFloat( CSSFloat cssFloat ); CSSClear getCSSClear() const { return mClear; } + void setCSSClear( CSSClear cssClear ); const Rectf& getOffsets() const { return mOffsets; } + void setOffsets( const Rectf& offsets ); int getZIndex() const { return mZIndex; } + void setZIndex( int zIndex ); virtual std::vector getPropertiesImplemented() const; @@ -57,10 +63,18 @@ class EE_API UIHTMLWidget : public UILayout { virtual void updateLayout(); + virtual void onParentChange(); + + virtual void onPositionChange(); + UIWidget* getContainingBlock(); void positionOutOfFlowChildren(); + void updateOutOfFlowPosition(); + + void updateStickyPosition(); + virtual RichText* getRichTextPtr() { return nullptr; } virtual bool isMergeable() const { return false; } @@ -84,6 +98,14 @@ class EE_API UIHTMLWidget : public UILayout { int mZIndex{ 0 }; UILayouter* mLayouter{ nullptr }; UnorderedMap mDataProperties; + + Uint32 mScrollCb{ 0 }; + Node* mScrollTarget{ nullptr }; + Vector2f mStickyBasePos; + bool mIsUpdatingScroll{ false }; + + void updateScrollListeners(); + void onScrollTargetPositionChange(); }; }} // namespace EE::UI diff --git a/include/eepp/ui/uirichtext.hpp b/include/eepp/ui/uirichtext.hpp index 304ac49fc..ee05ae535 100644 --- a/include/eepp/ui/uirichtext.hpp +++ b/include/eepp/ui/uirichtext.hpp @@ -178,6 +178,7 @@ class EE_API UIHTMLBody : public UIRichText { virtual Uint32 getType() const override; bool isType( const Uint32& type ) const override; bool applyProperty( const StyleSheetProperty& attribute ) override; + virtual void updateLayout() override; protected: bool mPropagatedBackground{ false }; diff --git a/src/eepp/ui/uihtmlwidget.cpp b/src/eepp/ui/uihtmlwidget.cpp index dc1b3de39..bd1b8c547 100644 --- a/src/eepp/ui/uihtmlwidget.cpp +++ b/src/eepp/ui/uihtmlwidget.cpp @@ -2,6 +2,8 @@ #include #include #include +#include +#include namespace EE { namespace UI { @@ -12,6 +14,8 @@ UIHTMLWidget* UIHTMLWidget::New() { UIHTMLWidget::UIHTMLWidget( const std::string& tag ) : UILayout( tag ) {} UIHTMLWidget::~UIHTMLWidget() { + if ( mScrollTarget && mScrollCb ) + mScrollTarget->removeEventListener( mScrollCb ); eeSAFE_DELETE( mLayouter ); } @@ -65,6 +69,7 @@ void UIHTMLWidget::setCSSPosition( CSSPosition position ) { if ( getLayoutWidthPolicy() == SizePolicy::MatchParent ) setLayoutWidthPolicy( SizePolicy::WrapContent ); } + updateScrollListeners(); onPositionChange(); } } @@ -201,17 +206,22 @@ void UIHTMLWidget::updateLayout() { UIWidget* UIHTMLWidget::getContainingBlock() { if ( mPosition == CSSPosition::Fixed ) { Node* parent = getParent(); - UIWidget* lastWidget = parent && parent->isWidget() ? parent->asType() : nullptr; + UIWidget* cb = parent && parent->isWidget() ? parent->asType() : nullptr; while ( parent ) { + if ( parent->isType( UI_TYPE_SCROLLVIEW ) ) { + cb = parent->asType(); + break; + } if ( parent->isWidget() ) - lastWidget = parent->asType(); + cb = parent->asType(); parent = parent->getParent(); } - return lastWidget; + return cb; } Node* parent = getParent(); UIWidget* lastWidget = nullptr; + UIWidget* htmlWidget = nullptr; while ( parent ) { if ( parent->isWidget() ) { lastWidget = parent->asType(); @@ -219,11 +229,14 @@ UIWidget* UIHTMLWidget::getContainingBlock() { if ( lastWidget->asType()->getCSSPosition() != CSSPosition::Static ) { return lastWidget; } + if ( lastWidget->isType( UI_TYPE_HTML_HTML ) ) { + htmlWidget = lastWidget; + } } } parent = parent->getParent(); } - return lastWidget; + return htmlWidget ? htmlWidget : lastWidget; } void UIHTMLWidget::positionOutOfFlowChildren() { @@ -233,66 +246,182 @@ void UIHTMLWidget::positionOutOfFlowChildren() { UIHTMLWidget* htmlChild = static_cast( child ); CSSPosition pos = htmlChild->getCSSPosition(); if ( pos == CSSPosition::Absolute || pos == CSSPosition::Fixed ) { - UIWidget* cb = htmlChild->getContainingBlock(); - if ( cb ) { - Rectf cbContentOffset = cb->getPixelsContentOffset(); - Float cbContentWidth = cb->getPixelsSize().getWidth() - cbContentOffset.Left - - cbContentOffset.Right; - Float cbContentHeight = cb->getPixelsSize().getHeight() - cbContentOffset.Top - - cbContentOffset.Bottom; - - Rectf margin = htmlChild->getLayoutPixelsMargin(); - Float childWidth = htmlChild->getPixelsSize().getWidth(); - Float childHeight = htmlChild->getPixelsSize().getHeight(); - - Float top = 0; - Float left = 0; - - bool useTop = htmlChild->mTopEq != "auto"; - bool useBottom = htmlChild->mBottomEq != "auto"; - bool useLeft = htmlChild->mLeftEq != "auto"; - bool useRight = htmlChild->mRightEq != "auto"; - - if ( useLeft ) { - left = htmlChild->lengthFromValue( - htmlChild->mLeftEq, CSS::PropertyRelativeTarget::ContainingBlockWidth, - 0 ); - } else if ( useRight ) { - Float rightVal = htmlChild->lengthFromValue( - htmlChild->mRightEq, CSS::PropertyRelativeTarget::ContainingBlockWidth, - 0 ); - left = cbContentWidth - childWidth - margin.Left - margin.Right - rightVal; - } - - if ( useTop ) { - top = htmlChild->lengthFromValue( - htmlChild->mTopEq, CSS::PropertyRelativeTarget::ContainingBlockHeight, - 0 ); - } else if ( useBottom ) { - Float bottomVal = htmlChild->lengthFromValue( - htmlChild->mBottomEq, - CSS::PropertyRelativeTarget::ContainingBlockHeight, 0 ); - top = - cbContentHeight - childHeight - margin.Top - margin.Bottom - bottomVal; - } - - top += margin.Top; - left += margin.Left; - - Vector2f cbPos( cbContentOffset.Left, cbContentOffset.Top ); - cbPos.x += left; - cbPos.y += top; - - Vector2f worldPos = cb->convertToWorldSpace( cbPos ); - Vector2f localPos = convertToNodeSpace( worldPos ); - htmlChild->setPixelsPosition( localPos ); - } + htmlChild->updateOutOfFlowPosition(); } } child = child->getNextNode(); } } +void UIHTMLWidget::updateOutOfFlowPosition() { + UIWidget* cb = getContainingBlock(); + if ( !cb ) + return; + + Rectf cbContentOffset = cb->getPixelsContentOffset(); + Float cbContentWidth = + cb->getPixelsSize().getWidth() - cbContentOffset.Left - cbContentOffset.Right; + Float cbContentHeight = + cb->getPixelsSize().getHeight() - cbContentOffset.Top - cbContentOffset.Bottom; + + Rectf margin = getLayoutPixelsMargin(); + Float childWidth = getPixelsSize().getWidth(); + Float childHeight = getPixelsSize().getHeight(); + + Float top = 0; + Float left = 0; + + bool useTop = mTopEq != "auto"; + bool useBottom = mBottomEq != "auto"; + bool useLeft = mLeftEq != "auto"; + bool useRight = mRightEq != "auto"; + + if ( useLeft ) { + left = lengthFromValue( mLeftEq, CSS::PropertyRelativeTarget::ContainingBlockWidth, 0 ); + } else if ( useRight ) { + Float rightVal = + lengthFromValue( mRightEq, CSS::PropertyRelativeTarget::ContainingBlockWidth, 0 ); + left = cbContentWidth - childWidth - margin.Left - margin.Right - rightVal; + } + + if ( useTop ) { + top = lengthFromValue( mTopEq, CSS::PropertyRelativeTarget::ContainingBlockHeight, 0 ); + } else if ( useBottom ) { + Float bottomVal = + lengthFromValue( mBottomEq, CSS::PropertyRelativeTarget::ContainingBlockHeight, 0 ); + top = cbContentHeight - childHeight - margin.Top - margin.Bottom - bottomVal; + } + + top += margin.Top; + left += margin.Left; + + Vector2f cbPos( cbContentOffset.Left, cbContentOffset.Top ); + cbPos.x += left; + cbPos.y += top; + + Vector2f worldPos = cb->convertToWorldSpace( cbPos ); + Vector2f localPos = getParent()->convertToNodeSpace( worldPos ); + setPixelsPosition( localPos ); +} + +void UIHTMLWidget::updateStickyPosition() { + if ( !mScrollTarget ) + return; + + UIWidget* cb = getContainingBlock(); + if ( !cb ) + return; + + Vector2f baseWorldPos = getParent()->convertToWorldSpace( mStickyBasePos ); + + Node* viewport = mScrollTarget->getParent(); + if ( !viewport ) + return; + + Vector2f posInViewport = viewport->convertToNodeSpace( baseWorldPos ); + + Float topOffset = 0; + bool useTop = mTopEq != "auto"; + if ( useTop ) + topOffset = + lengthFromValue( mTopEq, CSS::PropertyRelativeTarget::ContainingBlockHeight, 0 ); + + Float bottomOffset = 0; + bool useBottom = mBottomEq != "auto"; + if ( useBottom ) + bottomOffset = + lengthFromValue( mBottomEq, CSS::PropertyRelativeTarget::ContainingBlockHeight, 0 ); + + Vector2f newPosInViewport = posInViewport; + + if ( useTop ) { + if ( posInViewport.y < topOffset ) { + newPosInViewport.y = topOffset; + } + } + + if ( useBottom ) { + Float viewportHeight = viewport->getSize().getHeight(); + if ( posInViewport.y + getPixelsSize().getHeight() > viewportHeight - bottomOffset ) { + newPosInViewport.y = viewportHeight - bottomOffset - getPixelsSize().getHeight(); + } + } + + Vector2f cbWorldPos = cb->convertToWorldSpace( Vector2f( 0, 0 ) ); + Vector2f cbInViewport = viewport->convertToNodeSpace( cbWorldPos ); + Float cbBottomInViewport = + cbInViewport.y + cb->getPixelsSize().getHeight() - cb->getPixelsPadding().Bottom; + + if ( newPosInViewport.y + getPixelsSize().getHeight() > cbBottomInViewport ) { + newPosInViewport.y = cbBottomInViewport - getPixelsSize().getHeight(); + } + + if ( newPosInViewport.y < cbInViewport.y + cb->getPixelsPadding().Top ) { + newPosInViewport.y = cbInViewport.y + cb->getPixelsPadding().Top; + } + + if ( newPosInViewport != posInViewport ) { + Vector2f newWorldPos = viewport->convertToWorldSpace( newPosInViewport ); + Vector2f newLocalPos = getParent()->convertToNodeSpace( newWorldPos ); + + mIsUpdatingScroll = true; + setPixelsPosition( newLocalPos ); + mIsUpdatingScroll = false; + } else { + mIsUpdatingScroll = true; + setPixelsPosition( mStickyBasePos ); + mIsUpdatingScroll = false; + } +} + +void UIHTMLWidget::updateScrollListeners() { + if ( mScrollTarget ) { + if ( mScrollCb ) { + mScrollTarget->removeEventListener( mScrollCb ); + mScrollCb = 0; + } + mScrollTarget = nullptr; + } + + if ( mPosition == CSSPosition::Fixed || mPosition == CSSPosition::Sticky ) { + Node* parent = getParent(); + while ( parent ) { + if ( parent->isType( UI_TYPE_SCROLLVIEW ) ) { + mScrollTarget = parent->asType()->getScrollView(); + break; + } + parent = parent->getParent(); + } + + if ( mScrollTarget ) { + mScrollCb = mScrollTarget->on( Event::OnPositionChange, [this]( const Event* ) { + onScrollTargetPositionChange(); + } ); + } + } +} + +void UIHTMLWidget::onParentChange() { + UILayout::onParentChange(); + updateScrollListeners(); +} + +void UIHTMLWidget::onPositionChange() { + UILayout::onPositionChange(); + if ( mPosition == CSSPosition::Sticky && !mIsUpdatingScroll ) { + mStickyBasePos = getPixelsPosition(); + updateStickyPosition(); + } +} + +void UIHTMLWidget::onScrollTargetPositionChange() { + if ( mPosition == CSSPosition::Fixed ) { + updateOutOfFlowPosition(); + } else if ( mPosition == CSSPosition::Sticky ) { + updateStickyPosition(); + } +} + void UIHTMLWidget::invalidateIntrinsicSize() { if ( mLayouter ) mLayouter->invalidateIntrinsicWidths(); diff --git a/src/eepp/ui/uirichtext.cpp b/src/eepp/ui/uirichtext.cpp index 64e7809e8..f9d4a9df5 100644 --- a/src/eepp/ui/uirichtext.cpp +++ b/src/eepp/ui/uirichtext.cpp @@ -112,6 +112,36 @@ bool UIHTMLBody::applyProperty( const StyleSheetProperty& attribute ) { return UIRichText::applyProperty( attribute ); } +void UIHTMLBody::updateLayout() { + UIRichText::updateLayout(); + + if ( mChild && mChild->isWidget() ) { + Float maxH = 0; + Node* child = mChild; + while ( child ) { + if ( child->isWidget() ) { + UIWidget* widget = child->asType(); + bool isFixed = false; + if ( widget->isType( UI_TYPE_HTML_WIDGET ) && + widget->asType()->getCSSPosition() == CSSPosition::Fixed ) { + isFixed = true; + } + if ( !isFixed ) { + Float childH = + widget->getPixelsPosition().y + widget->getPixelsSize().getHeight(); + maxH = std::max( maxH, childH ); + } + } + child = child->getNextNode(); + } + if ( maxH > 0 ) { + Float dpH = PixelDensity::pxToDp( maxH ); + if ( dpH != getMinSize().getHeight() ) + setMinHeight( dpH ); + } + } +} + UIRichText* UIRichText::NewHtml() { auto* html = UIHTMLHtml::New( "html" ); html->setClipType( ClipType::None ); diff --git a/src/tests/unit_tests/uihtml_position_tests.cpp b/src/tests/unit_tests/uihtml_position_tests.cpp index 235622f3c..f9bf33cc5 100644 --- a/src/tests/unit_tests/uihtml_position_tests.cpp +++ b/src/tests/unit_tests/uihtml_position_tests.cpp @@ -8,6 +8,8 @@ #include #include #include +#include +#include #include #include #include @@ -351,3 +353,88 @@ UTEST( UIHTMLWidget, positionOutOfFlow_RightBottomWithMargin ) { Engine::destroySingleton(); } + +UTEST( UIHTMLWidget, positionOutOfFlow_FixedScroll ) { + init_ui_test(); + UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode(); + + UIScrollView* scrollView = UIScrollView::New(); + scrollView->setParent( sceneNode->getRoot() ); + scrollView->setPixelsSize( 800, 600 ); + + UIHTMLBody* body = UIHTMLBody::New( "body" ); + body->setParent( scrollView ); + body->applyProperty( StyleSheetProperty( "position", "relative" ) ); + + UIWidget* dummyChild = UIWidget::New(); + dummyChild->setParent( body ); + dummyChild->setPixelsSize( 800, 2000 ); + dummyChild->setPixelsPosition( 0, 0 ); + + UIHTMLWidget* fixedChild = UIHTMLWidget::New(); + fixedChild->setParent( body ); + fixedChild->setPixelsSize( 100, 50 ); + fixedChild->applyProperty( StyleSheetProperty( "position", "fixed" ) ); + fixedChild->applyProperty( StyleSheetProperty( "top", "50px" ) ); + fixedChild->applyProperty( StyleSheetProperty( "left", "50px" ) ); + + sceneNode->updateDirtyLayouts(); + + // Scroll down by 200px + scrollView->getScrollView()->setPosition( { 0, -200 } ); + + Vector2f worldPos = fixedChild->convertToWorldSpace( { 0, 0 } ); + EXPECT_NEAR( 50.f, worldPos.x, 1.f ); + EXPECT_NEAR( 50.f, worldPos.y, 1.f ); + + Engine::destroySingleton(); +} + +UTEST( UIHTMLWidget, positionOutOfFlow_StickyScroll ) { + init_ui_test(); + UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode(); + + UIScrollView* scrollView = UIScrollView::New(); + scrollView->setParent( sceneNode->getRoot() ); + scrollView->setPixelsSize( 800, 600 ); + + UIHTMLBody* body = UIHTMLBody::New( "body" ); + body->setParent( scrollView ); + body->applyProperty( StyleSheetProperty( "position", "relative" ) ); + + UIHTMLWidget* stickyChild = UIHTMLWidget::New(); + stickyChild->setParent( body ); + stickyChild->setPixelsSize( 100, 50 ); + stickyChild->applyProperty( StyleSheetProperty( "margin-top", "100px" ) ); + stickyChild->applyProperty( StyleSheetProperty( "position", "sticky" ) ); + stickyChild->applyProperty( StyleSheetProperty( "top", "20px" ) ); + + UIWidget* dummyChild = UIWidget::New(); + dummyChild->setParent( body ); + dummyChild->setPixelsSize( 800, 2000 ); + dummyChild->setPixelsPosition( 0, 0 ); + + sceneNode->updateDirtyLayouts(); + + // Force base pos (as if layouter did it) + stickyChild->setPixelsPosition( 0, 100 ); + + // Ensure base pos was captured correctly + EXPECT_NEAR( 100.f, stickyChild->getPixelsPosition().y, 1.f ); + + // Scroll down by 50px + Float actualMaxScrollY = + body->getPixelsSize().getHeight() - scrollView->getContainer()->getPixelsSize().getHeight(); + scrollView->getVerticalScrollBar()->setValue( 50.f / actualMaxScrollY ); + + Vector2f worldPos1 = stickyChild->convertToWorldSpace( { 0, 0 } ); + EXPECT_NEAR( 50.f, worldPos1.y, 1.f ); + + // Scroll down by 150px + scrollView->getVerticalScrollBar()->setValue( 150.f / actualMaxScrollY ); + + Vector2f worldPos2 = stickyChild->convertToWorldSpace( { 0, 0 } ); + EXPECT_NEAR( 20.f, worldPos2.y, 1.f ); + + Engine::destroySingleton(); +} diff --git a/src/tests/unit_tests/uihtml_tests.cpp b/src/tests/unit_tests/uihtml_tests.cpp index 1468bf5c6..38e9ad12a 100644 --- a/src/tests/unit_tests/uihtml_tests.cpp +++ b/src/tests/unit_tests/uihtml_tests.cpp @@ -1173,3 +1173,71 @@ UTEST( UIHTML, InlineBlockBrowserTest ) { Engine::destroySingleton(); } + +UTEST( UIHTML, HeightExpansion ) { + Engine::instance()->createWindow( WindowSettings( 1024, 653, "Height Expansion Test", + WindowStyle::Default, WindowBackend::Default, + 32, {}, 1, false, true ), + ContextSettings( false, 0, 0, GLv_default, true, false ) ); + + UI::UISceneNode* sceneNode = init_test_inline_block(); + sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/ensoft/" ); + + std::string html; + FileSystem::fileGet( "assets/html/ensoft/ensoft.html", html ); + + sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); + sceneNode->update( Seconds( 1 ) ); + + // Wait a bit and update again to make sure layouts are computed + sceneNode->updateDirtyLayouts(); + + auto htmlNode = sceneNode->getRoot()->findByType( UI_TYPE_HTML_HTML ); + auto bodyNode = sceneNode->getRoot()->findByType( UI_TYPE_HTML_BODY ); + + ASSERT_TRUE( htmlNode != nullptr ); + ASSERT_TRUE( bodyNode != nullptr ); + + auto htmlWidget = htmlNode->asType(); + auto bodyWidget = bodyNode->asType(); + + EXPECT_GT( htmlWidget->getSize().getHeight(), 0 ); + EXPECT_GT( bodyWidget->getSize().getHeight(), 0 ); + + EXPECT_GE( htmlWidget->getSize().getHeight(), bodyWidget->getSize().getHeight() ); + + Engine::destroySingleton(); +} + +UTEST( UIHTML, HeightExpansion_FixedDoesNotExpand ) { + Engine::instance()->createWindow( WindowSettings( 1024, 653, "Height Expansion Test", + WindowStyle::Default, WindowBackend::Default, + 32, {}, 1, false, true ), + ContextSettings( false, 0, 0, GLv_default, true, false ) ); + + UI::UISceneNode* sceneNode = init_test_inline_block(); + + const std::string html = R"HTML( + + + +
+
+ + +)HTML"; + + sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); + sceneNode->update( Seconds( 1 ) ); + sceneNode->updateDirtyLayouts(); + + auto bodyNode = sceneNode->getRoot()->findByType( UI_TYPE_HTML_BODY ); + ASSERT_TRUE( bodyNode != nullptr ); + + auto bodyWidget = bodyNode->asType(); + + // The height should be 100px, not 550px because the fixed div should be ignored. + EXPECT_NEAR( bodyWidget->getPixelsSize().getHeight(), 100.f, 1.f ); + + Engine::destroySingleton(); +}