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

+
+
+ +
+
+ + + + +
RSS
+ + +
+ + 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 000000000..064b9295c Binary files /dev/null and b/bin/unit_tests/assets/html/ensoft/fx.png differ diff --git a/bin/unit_tests/assets/html/ensoft/logo.png b/bin/unit_tests/assets/html/ensoft/logo.png new file mode 100644 index 000000000..68b688c7e Binary files /dev/null and b/bin/unit_tests/assets/html/ensoft/logo.png differ 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 000000000..f195a1ce2 Binary files /dev/null and b/bin/unit_tests/assets/html/ensoft/rss.png differ 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(); +}