diff --git a/README.es.md b/README.es.md index 6f857f7..370b954 100644 --- a/README.es.md +++ b/README.es.md @@ -130,9 +130,12 @@ con GTK4 y ha terminado siendo algo que uso de verdad para borradores y notas. - **Exportar a PDF** (Ctrl+P): imprime o exporta el documento a través del pipeline de impresión de GTK, reutilizando el mismo análisis de tramos y adornos del editor. Las cabeceras, los bloques de código, las citas y las - tablas se tematizan, con ajustes de exportación separados (esquema de color, - fuentes, tamaño de página y orientación) y una paginación cuidadosa (sin + tablas se tematizan, y la maquetación se elige con plantillas de página + (Artículo, Manuscrito, Carta, Compacta o Personalizada), temas de hoja de + estilo y, si hace falta, un CSS propio. La paginación es cuidadosa (sin cabeceras huérfanas, soporte de `\newpage`). +- **Vista previa de impresión**: la misma maquetación del PDF, paginada, antes + de imprimir. - **Archivos**: abrir, guardar y guardar como con `GtkFileDialog`, escritura atómica, aviso de cambios sin guardar y autoguardado configurable. - **Barra lateral** (F9) con documentos recientes filtrables e índice del @@ -162,6 +165,49 @@ se encogen en vez de ocultarse: el glifo diminuto sigue en la maquetación de GTK, así que el camino del aborto por texto invisible de GNOME/gtk#8346 queda inalcanzable por construcción. +## Plantillas, temas y hojas de estilo + +El aspecto del PDF se decide en dos capas que se suman. La **plantilla** fija la +geometría: márgenes, interlineado, escala de cabeceras, cabecera y pie con +numeración, y bloque de título a partir del nombre del documento. El **tema** +fija tipografía y color. Cualquier tema se combina con cualquier plantilla. + +| Plantilla | Para qué | +| --- | --- | +| Personalizada | Usa los ajustes granulares de Preferencias | +| Artículo | La maquetación de siempre | +| Manuscrito | Serif, interlineado doble, márgenes de una pulgada, cabecera con el título y pie con número de página | +| Carta | Sin bloque de título, márgenes ajustados y pie con número de página | +| Compacta | Notas técnicas, espaciado apretado | + +| Tema | Qué hace | +| --- | --- | +| Ninguno | Deja la plantilla tal cual | +| Sepia | Papel crema, serif cálida, interlineado amplio y cabeceras grandes para leer mucho rato | +| Alto contraste | Texto sans más grande, negro sobre blanco y enlaces azules, para imprimir | +| Técnico | Todo monoespaciado, espaciado apretado y cabeceras azul marino, para notas y código | +| Académico | Serif de 11 pt con interlineado doble y cabeceras discretas, como una página de journal | + +La fila **Hoja de estilo** del mismo grupo carga un CSS del disco que se aplica +**encima** del tema, así que puede corregir un solo detalle. El analizador es un subconjunto +mapeado, no un motor CSS: el export pinta y pagina con cairo, de modo que cada +declaración se traduce a un valor que la maquetación ya entiende y lo que queda +fuera se ignora con un aviso. Quitar el fichero o elegir Ninguno devuelve la +salida anterior tal cual. + +| Selector | Propiedades | +| --- | --- | +| `@page` | `margin`, `margin-top`, `margin-right`, `margin-bottom`, `margin-left` (`pt`, `mm`, `cm`, `in`, `px`) | +| `body`, `html`, `:root` | `font-family`, `font-size`, `line-height`, `color`, `background-color`, `margin-bottom`, `--block-spacing`, `--muted-color` | +| `p` | `line-height`, `color`, `margin-bottom`, `--block-spacing` | +| `h1` a `h4` | `font-size`, `color` | +| `pre`, `code` | `font-family`, `color`, `background-color` | +| `blockquote` | `color`, `border-color` (la barra de la cita) | +| `a` | `color` | +| `table` | `color`, `background-color` | +| `th` | `color`, `background-color` | +| `hr` | `color` | + ## Limitaciones - **Solo UTF-8.** Los ficheros se leen como UTF-8 y se escriben de vuelta byte diff --git a/README.md b/README.md index 97ff1ac..2038997 100644 --- a/README.md +++ b/README.md @@ -155,22 +155,41 @@ Markers replaced by an ornament (bullets, `[x]`, `---`, `>`, fences, pipes) are shrunk rather than hidden: the tiny glyph stays in GTK's layout, so the invisible-text abort path of GNOME/gtk#8346 is unreachable by construction. -## Stylesheets +## Themes and stylesheets -`Preferences > Export > Stylesheet` loads a CSS file that is applied on top of -the page template. It is a mapped subset, not a CSS engine: the export paints -and paginates with cairo, so each declaration is translated into a value the -layout already understands, and anything outside this table is ignored (the -export warns once and keeps going). +Export styles come in two layers that stack: a page template decides the +geometry (margins, leading, heading sizes, running header and footer, title +block), and a theme decides typography and colour. `Preferences > Export > Theme` +offers four bundled themes, written with our own values and compiled into the +binary so there is nothing extra to install: + +| Theme | What it does | +| --- | --- | +| None | Leave the template as it is | +| Sepia | Cream paper, warm serif, wide leading and large headings for long reading | +| High contrast | Bigger sans text, black on white, blue links, for printing | +| Technical | Everything monospaced with tight spacing and navy headings, for notes and code | +| Academic | Eleven point serif with double leading and small headings, like a journal page | + +Any theme combines with any template. The `Stylesheet` row of the same group +takes a CSS file from disk, applied on top of the theme, so it can correct a +single detail of it. The parser is a mapped subset, not a CSS engine: the +export paints and paginates with cairo, so each declaration is translated into a +value the layout already understands, and anything outside this table is ignored +(the export warns once and keeps going). | Selector | Properties | | --- | --- | | `@page` | `margin`, `margin-top`, `margin-right`, `margin-bottom`, `margin-left` (`pt`, `mm`, `cm`, `in`, `px`) | -| `body`, `html`, `:root` | `font-family`, `font-size`, `line-height`, `color`, `background-color`, `margin-bottom`, `--block-spacing` | +| `body`, `html`, `:root` | `font-family`, `font-size`, `line-height`, `color`, `background-color`, `margin-bottom`, `--block-spacing`, `--muted-color` | | `p` | `line-height`, `color`, `margin-bottom`, `--block-spacing` | | `h1` to `h4` | `font-size`, `color` | | `pre`, `code` | `font-family`, `color`, `background-color` | -| `blockquote`, `a`, `table` | `color`, plus `background-color` on `table` | +| `blockquote` | `color`, `border-color` (the quote bar) | +| `a` | `color` | +| `table` | `color`, `background-color` | +| `th` | `color`, `background-color` | +| `hr` | `color` | When the sheet says nothing about a value, the template keeps its own, and removing the file restores the previous output exactly. Example: diff --git a/data/io.github.gnacho.Scribe.gschema.xml b/data/io.github.gnacho.Scribe.gschema.xml index 95b0916..b6b0a93 100644 --- a/data/io.github.gnacho.Scribe.gschema.xml +++ b/data/io.github.gnacho.Scribe.gschema.xml @@ -41,6 +41,13 @@ + + + + + + + @@ -163,6 +170,10 @@ 'custom' PDF export page template + + 'none' + PDF export stylesheet theme applied on top of the page template + '' Path of the user stylesheet applied on top of the page template diff --git a/data/themes/academic.css b/data/themes/academic.css new file mode 100644 index 0000000..57d1ca1 --- /dev/null +++ b/data/themes/academic.css @@ -0,0 +1,41 @@ +/* Tema Académico: serif sobria, cabeceras pequeñas e interlineado doble, como un paper. */ + +body { + font-family: "Source Serif 4", Georgia, serif; + font-size: 11pt; + line-height: 2; + color: #111111; + background-color: #ffffff; + --muted-color: #555555; + --block-spacing: 10pt; +} + +h1, +h2, +h3, +h4 { + color: #111111; +} + +h1 { font-size: 15pt; } +h2 { font-size: 13pt; } +h3 { font-size: 11.5pt; } +h4 { font-size: 11pt; } + +pre, +code { + color: #222222; + background-color: #f4f4f4; +} + +blockquote { + color: #333333; + border-color: #999999; +} + +a { color: #1a3d6d; } + +table { background-color: #ffffff; } +table th { background-color: #f0f0f0; } + +hr { color: #bbbbbb; } diff --git a/data/themes/high-contrast.css b/data/themes/high-contrast.css new file mode 100644 index 0000000..2567093 --- /dev/null +++ b/data/themes/high-contrast.css @@ -0,0 +1,41 @@ +/* Tema Alto contraste: negro sobre blanco y texto grande, para imprimir y para vista cansada. */ + +body { + font-family: sans-serif; + font-size: 14pt; + line-height: 1.65; + color: #000000; + background-color: #ffffff; + --muted-color: #1f1f1f; + --block-spacing: 16pt; +} + +h1, +h2, +h3, +h4 { + color: #000000; +} + +h1 { font-size: 28pt; } +h2 { font-size: 21pt; } +h3 { font-size: 17pt; } +h4 { font-size: 14pt; } + +pre, +code { + color: #000000; + background-color: #e8e8e8; +} + +blockquote { + color: #000000; + border-color: #000000; +} + +a { color: #0000ee; } + +table { background-color: #ffffff; } +table th { background-color: #e0e0e0; } + +hr { color: #000000; } diff --git a/data/themes/sepia.css b/data/themes/sepia.css new file mode 100644 index 0000000..1e9bd46 --- /dev/null +++ b/data/themes/sepia.css @@ -0,0 +1,41 @@ +/* Tema Sepia: papel crema, serif cálida y mucho aire, para leer largo rato. */ + +body { + font-family: "Source Serif 4", Georgia, serif; + font-size: 12pt; + line-height: 1.75; + color: #43362a; + background-color: #f6efe0; + --muted-color: #85705a; + --block-spacing: 13pt; +} + +h1, +h2, +h3, +h4 { + color: #6b4f31; +} + +h1 { font-size: 25pt; } +h2 { font-size: 18.5pt; } +h3 { font-size: 14.5pt; } +h4 { font-size: 12.5pt; } + +pre, +code { + color: #4a3b2c; + background-color: #ece1cb; +} + +blockquote { + color: #6b5a45; + border-color: #c2ab84; +} + +a { color: #8a5a2b; } + +table { background-color: #f1e8d6; } +table th { background-color: #e4d7bc; } + +hr { color: #d3c19f; } diff --git a/data/themes/technical.css b/data/themes/technical.css new file mode 100644 index 0000000..5c0ddad --- /dev/null +++ b/data/themes/technical.css @@ -0,0 +1,41 @@ +/* Tema Técnico: todo monoespaciado y apretado, para notas, logs y documentación de código. */ + +body { + font-family: "Adwaita Mono", monospace; + font-size: 9.5pt; + line-height: 1.5; + color: #1d1f21; + background-color: #ffffff; + --muted-color: #5f6773; + --block-spacing: 6pt; +} + +h1, +h2, +h3, +h4 { + color: #0b3d91; +} + +h1 { font-size: 15pt; } +h2 { font-size: 12.5pt; } +h3 { font-size: 11pt; } +h4 { font-size: 10pt; } + +pre, +code { + color: #1f2937; + background-color: #eef1f5; +} + +blockquote { + color: #47505c; + border-color: #b8c0cc; +} + +a { color: #0b63c5; } + +table { background-color: #f7f9fb; } +table th { background-color: #e6ebf1; } + +hr { color: #b8c0cc; } diff --git a/po/es.po b/po/es.po index 4dd4a68..22b84c8 100644 --- a/po/es.po +++ b/po/es.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: scribe\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-10-27 06:01+0100\n" +"POT-Creation-Date: 2026-10-27 16:20+0100\n" "PO-Revision-Date: 2026-09-19 19:58+0200\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" @@ -17,19 +17,19 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: src/main.rs:277 +#: src/main.rs:278 msgid "Recover unsaved documents?" msgstr "¿Recuperar los documentos sin guardar?" -#: src/main.rs:280 +#: src/main.rs:281 msgid "_Recover All" msgstr "_Recuperar todo" -#: src/main.rs:281 +#: src/main.rs:282 msgid "_Discard All" msgstr "_Descartar todo" -#: src/main.rs:282 +#: src/main.rs:283 msgid "Decide Later" msgstr "Decidir más tarde" @@ -286,7 +286,7 @@ msgstr "Apariencia" msgid "Theme" msgstr "Tema" -#: src/preferences.rs:59 src/preferences.rs:437 +#: src/preferences.rs:59 src/preferences.rs:439 msgid "Color scheme" msgstr "Esquema de color" @@ -294,15 +294,15 @@ msgstr "Esquema de color" msgid "Follows the system unless you force one" msgstr "Sigue al sistema salvo que fuerces uno" -#: src/preferences.rs:61 src/preferences.rs:440 +#: src/preferences.rs:61 src/preferences.rs:442 msgid "System" msgstr "Sistema" -#: src/preferences.rs:61 src/preferences.rs:441 +#: src/preferences.rs:61 src/preferences.rs:443 msgid "Light" msgstr "Claro" -#: src/preferences.rs:61 src/preferences.rs:442 +#: src/preferences.rs:61 src/preferences.rs:444 msgid "Dark" msgstr "Oscuro" @@ -326,23 +326,23 @@ msgstr "Texto" msgid "Code and tables always use a monospaced font" msgstr "El código y las tablas usan siempre monoespaciada" -#: src/preferences.rs:114 src/preferences.rs:448 +#: src/preferences.rs:114 src/preferences.rs:450 msgid "Font family" msgstr "Familia tipográfica" -#: src/preferences.rs:115 src/preferences.rs:449 +#: src/preferences.rs:115 src/preferences.rs:451 msgid "Document body" msgstr "Cuerpo del documento" -#: src/preferences.rs:117 src/preferences.rs:451 +#: src/preferences.rs:117 src/preferences.rs:453 msgid "Sans (Cantarell)" msgstr "Sans (Cantarell)" -#: src/preferences.rs:118 src/preferences.rs:452 +#: src/preferences.rs:118 src/preferences.rs:454 msgid "Serif" msgstr "Serif" -#: src/preferences.rs:119 src/preferences.rs:453 +#: src/preferences.rs:119 src/preferences.rs:455 msgid "Monospace" msgstr "Monoespaciada" @@ -404,7 +404,8 @@ msgstr "Atenuar siempre" #: src/preferences.rs:229 msgid "Hiding text still crashes this GTK: the marks shrink instead" -msgstr "Ocultar texto aún aborta con este GTK: las marcas se encogen en su lugar" +msgstr "" +"Ocultar texto aún aborta con este GTK: las marcas se encogen en su lugar" #: src/preferences.rs:231 msgid "Always hide (shrinks the marks; GTK cannot hide them yet)" @@ -495,117 +496,153 @@ msgstr "Documentos recientes" msgid "Clears the list shown in the sidebar" msgstr "Borra la lista que aparece en la barra lateral" -#: src/preferences.rs:401 +#: src/preferences.rs:403 src/preferences.rs:492 msgid "None" msgstr "Ninguna" -#: src/preferences.rs:413 +#: src/preferences.rs:415 msgid "Export" msgstr "Exportar" -#: src/preferences.rs:418 +#: src/preferences.rs:420 msgid "PDF" msgstr "PDF" -#: src/preferences.rs:420 +#: src/preferences.rs:422 msgid "These settings apply to PDF export only, not to the editor view" msgstr "" "Estos ajustes se aplican solo a la exportación a PDF, no a la vista del " "editor" -#: src/preferences.rs:425 +#: src/preferences.rs:427 msgid "Template" msgstr "Plantilla" -#: src/preferences.rs:426 +#: src/preferences.rs:428 msgid "Page layout preset; Custom uses the settings below" msgstr "Ajuste de página predefinido; Personalizada usa los ajustes de abajo" -#: src/preferences.rs:428 +#: src/preferences.rs:430 msgid "Custom" msgstr "Personalizada" -#: src/preferences.rs:429 +#: src/preferences.rs:431 msgid "Article" msgstr "Artículo" -#: src/preferences.rs:430 +#: src/preferences.rs:432 msgid "Manuscript" msgstr "Manuscrito" -#: src/preferences.rs:431 +#: src/preferences.rs:433 msgid "Letter" msgstr "Carta" -#: src/preferences.rs:432 +#: src/preferences.rs:434 msgid "Compact" msgstr "Compacta" -#: src/preferences.rs:438 +#: src/preferences.rs:440 msgid "Monochrome prints everything in black on white" msgstr "Monocromo imprime todo en negro sobre blanco" -#: src/preferences.rs:443 +#: src/preferences.rs:445 msgid "Monochrome" msgstr "Monocromo" -#: src/preferences.rs:458 +#: src/preferences.rs:460 msgid "Font size" msgstr "Tamaño de fuente" -#: src/preferences.rs:459 +#: src/preferences.rs:461 msgid "Points" msgstr "Puntos" -#: src/preferences.rs:464 +#: src/preferences.rs:466 msgid "Monospace font" msgstr "Fuente monoespaciada" -#: src/preferences.rs:465 +#: src/preferences.rs:467 msgid "Used for code blocks and tables" msgstr "Se usa en bloques de código y tablas" -#: src/preferences.rs:474 +#: src/preferences.rs:476 msgid "Page size" msgstr "Tamaño de página" -#: src/preferences.rs:475 +#: src/preferences.rs:477 msgid "Paper" msgstr "Papel" -#: src/preferences.rs:480 +#: src/preferences.rs:482 msgid "Orientation" msgstr "Orientación" -#: src/preferences.rs:481 +#: src/preferences.rs:483 msgid "Paper orientation" msgstr "Orientación del papel" -#: src/preferences.rs:482 +#: src/preferences.rs:484 msgid "Portrait" msgstr "Vertical" -#: src/preferences.rs:482 +#: src/preferences.rs:484 msgid "Landscape" msgstr "Horizontal" -#: src/preferences.rs:487 +#: src/preferences.rs:489 +msgid "PDF theme" +msgstr "Tema del PDF" + +#: src/preferences.rs:490 +msgid "Typography and colours on top of the template" +msgstr "Tipografía y colores encima de la plantilla" + +#: src/preferences.rs:493 +msgid "Sepia" +msgstr "Sepia" + +#: src/preferences.rs:494 +msgid "High contrast" +msgstr "Alto contraste" + +#: src/preferences.rs:495 +msgid "Technical" +msgstr "Técnico" + +#: src/preferences.rs:496 +msgid "Academic" +msgstr "Académico" + +#: src/preferences.rs:501 msgid "Stylesheet" msgstr "Hoja de estilo" -#: src/preferences.rs:491 +#: src/preferences.rs:505 msgid "Load…" msgstr "Cargar…" -#: src/preferences.rs:496 +#: src/preferences.rs:510 msgid "Use the template as is" msgstr "Usar la plantilla tal cual" -#: src/preferences.rs:591 +#: src/preferences.rs:542 +msgid "Typography and colour" +msgstr "Tipografía y color" + +#: src/preferences.rs:544 +msgid "" +"A theme changes type and colours on top of the template. Your own stylesheet " +"is applied last, so it wins over both" +msgstr "" +"El tema cambia la tipografía y los colores encima de la plantilla. Tu hoja " +"de estilo se aplica al final y manda sobre los dos" + +#: src/preferences.rs:628 msgid "CSS stylesheets" msgstr "Hojas de estilo CSS" -#: src/preferences.rs:596 +#: src/preferences.rs:633 msgid "Choose a stylesheet" msgstr "Elige una hoja de estilo" @@ -725,6 +762,9 @@ msgstr "Ampliar o reducir" msgid "Preferences" msgstr "Preferencias" +#~ msgid "Custom file…" +#~ msgstr "Fichero propio…" + #~ msgid "Solarized Dark" #~ msgstr "Solarized oscuro" diff --git a/po/scribe.pot b/po/scribe.pot index ca569ea..26825f0 100644 --- a/po/scribe.pot +++ b/po/scribe.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-10-27 06:01+0100\n" +"POT-Creation-Date: 2026-10-27 16:20+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,19 +17,19 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: src/main.rs:277 +#: src/main.rs:278 msgid "Recover unsaved documents?" msgstr "" -#: src/main.rs:280 +#: src/main.rs:281 msgid "_Recover All" msgstr "" -#: src/main.rs:281 +#: src/main.rs:282 msgid "_Discard All" msgstr "" -#: src/main.rs:282 +#: src/main.rs:283 msgid "Decide Later" msgstr "" @@ -286,7 +286,7 @@ msgstr "" msgid "Theme" msgstr "" -#: src/preferences.rs:59 src/preferences.rs:437 +#: src/preferences.rs:59 src/preferences.rs:439 msgid "Color scheme" msgstr "" @@ -294,15 +294,15 @@ msgstr "" msgid "Follows the system unless you force one" msgstr "" -#: src/preferences.rs:61 src/preferences.rs:440 +#: src/preferences.rs:61 src/preferences.rs:442 msgid "System" msgstr "" -#: src/preferences.rs:61 src/preferences.rs:441 +#: src/preferences.rs:61 src/preferences.rs:443 msgid "Light" msgstr "" -#: src/preferences.rs:61 src/preferences.rs:442 +#: src/preferences.rs:61 src/preferences.rs:444 msgid "Dark" msgstr "" @@ -326,23 +326,23 @@ msgstr "" msgid "Code and tables always use a monospaced font" msgstr "" -#: src/preferences.rs:114 src/preferences.rs:448 +#: src/preferences.rs:114 src/preferences.rs:450 msgid "Font family" msgstr "" -#: src/preferences.rs:115 src/preferences.rs:449 +#: src/preferences.rs:115 src/preferences.rs:451 msgid "Document body" msgstr "" -#: src/preferences.rs:117 src/preferences.rs:451 +#: src/preferences.rs:117 src/preferences.rs:453 msgid "Sans (Cantarell)" msgstr "" -#: src/preferences.rs:118 src/preferences.rs:452 +#: src/preferences.rs:118 src/preferences.rs:454 msgid "Serif" msgstr "" -#: src/preferences.rs:119 src/preferences.rs:453 +#: src/preferences.rs:119 src/preferences.rs:455 msgid "Monospace" msgstr "" @@ -494,115 +494,149 @@ msgstr "" msgid "Clears the list shown in the sidebar" msgstr "" -#: src/preferences.rs:401 +#: src/preferences.rs:403 src/preferences.rs:492 msgid "None" msgstr "" -#: src/preferences.rs:413 +#: src/preferences.rs:415 msgid "Export" msgstr "" -#: src/preferences.rs:418 +#: src/preferences.rs:420 msgid "PDF" msgstr "" -#: src/preferences.rs:420 +#: src/preferences.rs:422 msgid "These settings apply to PDF export only, not to the editor view" msgstr "" -#: src/preferences.rs:425 +#: src/preferences.rs:427 msgid "Template" msgstr "" -#: src/preferences.rs:426 +#: src/preferences.rs:428 msgid "Page layout preset; Custom uses the settings below" msgstr "" -#: src/preferences.rs:428 +#: src/preferences.rs:430 msgid "Custom" msgstr "" -#: src/preferences.rs:429 +#: src/preferences.rs:431 msgid "Article" msgstr "" -#: src/preferences.rs:430 +#: src/preferences.rs:432 msgid "Manuscript" msgstr "" -#: src/preferences.rs:431 +#: src/preferences.rs:433 msgid "Letter" msgstr "" -#: src/preferences.rs:432 +#: src/preferences.rs:434 msgid "Compact" msgstr "" -#: src/preferences.rs:438 +#: src/preferences.rs:440 msgid "Monochrome prints everything in black on white" msgstr "" -#: src/preferences.rs:443 +#: src/preferences.rs:445 msgid "Monochrome" msgstr "" -#: src/preferences.rs:458 +#: src/preferences.rs:460 msgid "Font size" msgstr "" -#: src/preferences.rs:459 +#: src/preferences.rs:461 msgid "Points" msgstr "" -#: src/preferences.rs:464 +#: src/preferences.rs:466 msgid "Monospace font" msgstr "" -#: src/preferences.rs:465 +#: src/preferences.rs:467 msgid "Used for code blocks and tables" msgstr "" -#: src/preferences.rs:474 +#: src/preferences.rs:476 msgid "Page size" msgstr "" -#: src/preferences.rs:475 +#: src/preferences.rs:477 msgid "Paper" msgstr "" -#: src/preferences.rs:480 +#: src/preferences.rs:482 msgid "Orientation" msgstr "" -#: src/preferences.rs:481 +#: src/preferences.rs:483 msgid "Paper orientation" msgstr "" -#: src/preferences.rs:482 +#: src/preferences.rs:484 msgid "Portrait" msgstr "" -#: src/preferences.rs:482 +#: src/preferences.rs:484 msgid "Landscape" msgstr "" -#: src/preferences.rs:487 +#: src/preferences.rs:489 +msgid "PDF theme" +msgstr "" + +#: src/preferences.rs:490 +msgid "Typography and colours on top of the template" +msgstr "" + +#: src/preferences.rs:493 +msgid "Sepia" +msgstr "" + +#: src/preferences.rs:494 +msgid "High contrast" +msgstr "" + +#: src/preferences.rs:495 +msgid "Technical" +msgstr "" + +#: src/preferences.rs:496 +msgid "Academic" +msgstr "" + +#: src/preferences.rs:501 msgid "Stylesheet" msgstr "" -#: src/preferences.rs:491 +#: src/preferences.rs:505 msgid "Load…" msgstr "" -#: src/preferences.rs:496 +#: src/preferences.rs:510 msgid "Use the template as is" msgstr "" -#: src/preferences.rs:591 +#: src/preferences.rs:542 +msgid "Typography and colour" +msgstr "" + +#: src/preferences.rs:544 +msgid "" +"A theme changes type and colours on top of the template. Your own stylesheet " +"is applied last, so it wins over both" +msgstr "" + +#: src/preferences.rs:628 msgid "CSS stylesheets" msgstr "" -#: src/preferences.rs:596 +#: src/preferences.rs:633 msgid "Choose a stylesheet" msgstr "" diff --git a/src/export_css.rs b/src/export_css.rs index 8661080..9606dbe 100644 --- a/src/export_css.rs +++ b/src/export_css.rs @@ -48,8 +48,13 @@ pub struct Colores { pub codigo: Option, pub codigo_fondo: Option, pub cita: Option, + pub cita_barra: Option, pub enlace: Option, + pub regla: Option, pub tabla_fondo: Option, + pub tabla_cabecera: Option, + /// Texto secundario: numeración de página, metadatos y adornos. + pub atenuado: Option, } /// Valores que una hoja de estilo impone a la maquetación. @@ -123,7 +128,9 @@ enum Elemento { Codigo, Cita, Enlace, + Regla, Tabla, + CeldaCabecera, } /// Quita los comentarios `/* ... */` y devuelve el resto como caracteres. @@ -239,7 +246,9 @@ fn elemento(selector: &str) -> Option { "pre" | "code" => Some(Elemento::Codigo), "blockquote" => Some(Elemento::Cita), "a" => Some(Elemento::Enlace), + "hr" => Some(Elemento::Regla), "table" => Some(Elemento::Tabla), + "th" => Some(Elemento::CeldaCabecera), _ => None, } } @@ -254,6 +263,13 @@ fn aplicar_regla(ajustes: &mut Ajustes, el: Elemento, prop: &str, valor: &str) - } return false; } + if prop == "--muted-color" { + if let Some(c) = color(&valor) { + ajustes.colores.atenuado = Some(c); + return true; + } + return false; + } match (el, prop) { (Elemento::Raiz, "font-family") => { if let Some(f) = familia(&valor) { @@ -300,6 +316,16 @@ fn aplicar_regla(ajustes: &mut Ajustes, el: Elemento, prop: &str, valor: &str) - ajustes.colores.tabla_fondo = Some(c); } } + (Elemento::CeldaCabecera, "background-color") => { + if let Some(c) = color(&valor) { + ajustes.colores.tabla_cabecera = Some(c); + } + } + (Elemento::Cita, "border-color") => { + if let Some(c) = color(&valor) { + ajustes.colores.cita_barra = Some(c); + } + } (_, "color") => { if let Some(c) = color(&valor) { *campo_color(&mut ajustes.colores, el) = Some(c); @@ -313,11 +339,14 @@ fn aplicar_regla(ajustes: &mut Ajustes, el: Elemento, prop: &str, valor: &str) - /// Campo de la paleta que alimenta el `color` de cada elemento. fn campo_color(colores: &mut Colores, el: Elemento) -> &mut Option { match el { - Elemento::Raiz | Elemento::Parrafo | Elemento::Tabla => &mut colores.texto, + Elemento::Raiz | Elemento::Parrafo | Elemento::Tabla | Elemento::CeldaCabecera => { + &mut colores.texto + } Elemento::Cabecera(_) => &mut colores.cabecera, Elemento::Codigo => &mut colores.codigo, Elemento::Cita => &mut colores.cita, Elemento::Enlace => &mut colores.enlace, + Elemento::Regla => &mut colores.regla, } } @@ -732,4 +761,91 @@ mod tests { assert_eq!(elemento("table td"), None); assert_eq!(elemento(""), None); } + + #[test] + fn css_el_texto_atenuado_sale_de_la_variable() { + let a = analizar("body { --muted-color: #7a6a58 }"); + assert_eq!( + a.colores.atenuado, + Some(( + 0x7a as f64 / 255.0, + 0x6a as f64 / 255.0, + 0x58 as f64 / 255.0 + )) + ); + assert!(a.ignoradas.is_empty()); + } + + #[test] + fn css_la_cabecera_de_tabla_y_los_filetes_tienen_su_color() { + let a = analizar("table th { background-color: #e7dcc6 } hr { color: #d8c9ae }"); + assert_eq!( + a.colores.tabla_cabecera, + Some(( + 0xe7 as f64 / 255.0, + 0xdc as f64 / 255.0, + 0xc6 as f64 / 255.0 + )) + ); + assert_eq!( + a.colores.regla, + Some(( + 0xd8 as f64 / 255.0, + 0xc9 as f64 / 255.0, + 0xae as f64 / 255.0 + )) + ); + assert!(a.ignoradas.is_empty()); + } + + #[test] + fn css_la_barra_de_la_cita_sale_del_borde() { + let a = analizar("blockquote { color: #6b5a45; border-color: #c9b696 }"); + assert_eq!( + a.colores.cita, + Some(( + 0x6b as f64 / 255.0, + 0x5a as f64 / 255.0, + 0x45 as f64 / 255.0 + )) + ); + assert_eq!( + a.colores.cita_barra, + Some(( + 0xc9 as f64 / 255.0, + 0xb6 as f64 / 255.0, + 0x96 as f64 / 255.0 + )) + ); + assert!(a.ignoradas.is_empty()); + } + + #[test] + fn css_una_hoja_con_varias_reglas_acumula_los_colores() { + let a = analizar( + "body { color: #111111; background-color: #ffffff } + h1, h2 { color: #222222 } + a { color: #1d4ed8 } + pre, code { background-color: #f3f4f6 }", + ); + assert_eq!( + a.colores.texto, + Some(( + 0x11 as f64 / 255.0, + 0x11 as f64 / 255.0, + 0x11 as f64 / 255.0 + )) + ); + assert_eq!( + a.colores.cabecera, + Some(( + 0x22 as f64 / 255.0, + 0x22 as f64 / 255.0, + 0x22 as f64 / 255.0 + )) + ); + assert!(a.colores.enlace.is_some()); + assert!(a.colores.codigo_fondo.is_some()); + assert!(a.ignoradas.is_empty()); + } } diff --git a/src/export_pdf.rs b/src/export_pdf.rs index b746a73..264d53b 100644 --- a/src/export_pdf.rs +++ b/src/export_pdf.rs @@ -52,7 +52,7 @@ use crate::export_css::Ajustes; use crate::markdown_render::{analyze, Analysis, Ornament, SpanKind}; use crate::settings::{ AppSettings, ExportColorScheme, ExportMonoFont, ExportOrientation, ExportPageSize, - ExportTemplate, FontFamily, + ExportTemplate, ExportTheme, FontFamily, }; // --------------------------------------------------------------------------- @@ -103,7 +103,9 @@ pub struct ExportConfig { pub papel: ExportPageSize, pub orientacion: ExportOrientation, pub template: ExportTemplate, - /// Hoja de estilo del usuario, aplicada encima de la plantilla. + /// Tema de hoja de estilo incluido, aplicado encima de la plantilla. + pub theme: ExportTheme, + /// Hoja de estilo del usuario, aplicada encima del tema. pub css_path: Option, } @@ -119,6 +121,7 @@ impl ExportConfig { papel: s.export_page_size(), orientacion: s.export_orientation(), template: s.export_template(), + theme: s.export_theme(), css_path: s.export_css_path(), } } @@ -386,12 +389,24 @@ fn aplicar_hoja( if let Some(v) = c.cita { paleta.cita = v; } + if let Some(v) = c.cita_barra { + paleta.cita_barra = v; + } if let Some(v) = c.enlace { paleta.enlace = v; } + if let Some(v) = c.regla { + paleta.regla = v; + } if let Some(v) = c.tabla_fondo { paleta.tabla_fondo = v; } + if let Some(v) = c.tabla_cabecera { + paleta.tabla_cabecera = v; + } + if let Some(v) = c.atenuado { + paleta.atenuado = v; + } } // --------------------------------------------------------------------------- @@ -1896,6 +1911,11 @@ pub fn crear_operacion( let mut plantilla = Plantilla::desde(config); let mut config = config.con_plantilla(&plantilla); let mut paleta = resolver_paleta(config.esquema, sistema_oscuro); + // Orden de capas: plantilla, tema incluido y, encima de todo, la hoja del + // usuario, que siempre puede corregir un detalle del tema. + if let Some(ajustes) = crate::export_themes::ajustes(config.theme) { + aplicar_hoja(&ajustes, &mut plantilla, &mut config, &mut paleta); + } let hoja = cargar_hoja(&config); if let Some(ajustes) = &hoja { aplicar_hoja(ajustes, &mut plantilla, &mut config, &mut paleta); @@ -2340,6 +2360,7 @@ mod tests { papel: ExportPageSize::A4, orientacion: ExportOrientation::Portrait, template, + theme: ExportTheme::None, css_path: None, } } @@ -2463,6 +2484,128 @@ mod tests { assert_eq!(ajustes.margenes.izquierda, Some(30.0)); } + /// Resolución de capas tal como la hace `crear_operacion`, sin GTK: + /// plantilla, tema incluido y hoja del usuario por encima. + fn resolver_capas(config: &ExportConfig) -> (Plantilla, ExportConfig, Paleta) { + let mut plantilla = Plantilla::desde(config); + let mut config = config.con_plantilla(&plantilla); + let mut paleta = resolver_paleta(config.esquema, false); + if let Some(ajustes) = crate::export_themes::ajustes(config.theme) { + aplicar_hoja(&ajustes, &mut plantilla, &mut config, &mut paleta); + } + if let Some(ajustes) = cargar_hoja(&config) { + aplicar_hoja(&ajustes, &mut plantilla, &mut config, &mut paleta); + } + (plantilla, config, paleta) + } + + #[test] + fn pdf_sin_tema_la_salida_es_la_de_siempre() { + let config = config_de(ExportTemplate::Article); + let (plantilla, resuelto, paleta) = resolver_capas(&config); + let referencia = Plantilla::desde(&config); + assert_eq!(plantilla.interlinea, referencia.interlinea); + assert_eq!(plantilla.espacio_bloque, referencia.espacio_bloque); + assert_eq!(plantilla.escala, referencia.escala); + assert_eq!(resuelto.familia_cuerpo, FontFamily::Sans); + assert_eq!(resuelto.tamano_cuerpo, 11.0); + assert_eq!( + paleta.texto, + resolver_paleta(ExportColorScheme::Light, false).texto + ); + } + + #[test] + fn pdf_el_tema_impone_tipografia_y_colores() { + let mut config = config_de(ExportTemplate::Article); + config.theme = ExportTheme::Sepia; + let (plantilla, resuelto, paleta) = resolver_capas(&config); + assert_eq!(resuelto.familia_cuerpo, FontFamily::Serif); + assert_eq!(resuelto.tamano_cuerpo, 12.0); + assert_eq!(plantilla.interlinea, 1.75); + assert_eq!( + paleta.fondo, + ( + 0xf6 as f64 / 255.0, + 0xef as f64 / 255.0, + 0xe0 as f64 / 255.0 + ) + ); + assert_eq!( + paleta.texto, + ( + 0x43 as f64 / 255.0, + 0x36 as f64 / 255.0, + 0x2a as f64 / 255.0 + ) + ); + assert!(paleta.atenuado != resolver_paleta(ExportColorScheme::Light, false).atenuado); + } + + #[test] + fn pdf_el_tema_no_toca_los_margenes_de_la_plantilla() { + let mut config = config_de(ExportTemplate::Manuscript); + config.theme = ExportTheme::Technical; + let (plantilla, _, _) = resolver_capas(&config); + assert_eq!(plantilla.margenes.map(|m| m.superior), Some(72.0)); + let mut config = config_de(ExportTemplate::Custom); + config.theme = ExportTheme::Technical; + let (plantilla, _, _) = resolver_capas(&config); + assert!(plantilla.margenes.is_none()); + } + + #[test] + fn pdf_la_hoja_del_usuario_gana_al_tema() { + let dir = std::env::temp_dir().join("scribe-tema-y-hoja"); + let _ = std::fs::create_dir_all(&dir); + let ruta = dir.join("encima.css"); + std::fs::write(&ruta, "body { color: #000000 } h1 { color: #ff0000 }") + .expect("escribir css"); + let mut config = config_de(ExportTemplate::Article); + config.theme = ExportTheme::Sepia; + config.css_path = Some(ruta.clone()); + let (_, _, paleta) = resolver_capas(&config); + assert_eq!(paleta.texto, (0.0, 0.0, 0.0)); + assert_eq!(paleta.cabecera, (1.0, 0.0, 0.0)); + // El fondo del tema sigue ahí: la hoja solo pisa lo que declara. + assert_eq!( + paleta.fondo, + ( + 0xf6 as f64 / 255.0, + 0xef as f64 / 255.0, + 0xe0 as f64 / 255.0 + ) + ); + let _ = std::fs::remove_file(&ruta); + } + + #[test] + fn pdf_cada_tema_se_combina_con_cada_plantilla() { + let temas = [ + ExportTheme::Sepia, + ExportTheme::HighContrast, + ExportTheme::Technical, + ExportTheme::Academic, + ]; + let plantillas = [ + ExportTemplate::Article, + ExportTemplate::Manuscript, + ExportTemplate::Letter, + ExportTemplate::Compact, + ]; + for tema in temas { + for plantilla in plantillas { + let mut config = config_de(plantilla); + config.theme = tema; + let (resuelta, resuelto, paleta) = resolver_capas(&config); + assert!(resuelto.tamano_cuerpo > 0.0, "{tema:?}/{plantilla:?}"); + assert!(resuelta.interlinea > 0.0, "{tema:?}/{plantilla:?}"); + assert!(resuelta.escala_cabecera(1) > 0.0, "{tema:?}/{plantilla:?}"); + assert!(paleta.fondo != paleta.texto, "{tema:?}/{plantilla:?}"); + } + } + } + #[test] fn pdf_una_hoja_que_no_se_puede_leer_no_cambia_el_export() { let mut config = config_de(ExportTemplate::Article); diff --git a/src/export_themes.rs b/src/export_themes.rs new file mode 100644 index 0000000..84ad2c9 --- /dev/null +++ b/src/export_themes.rs @@ -0,0 +1,98 @@ +//! Hojas de estilo incluidas en el binario. +//! +//! Cada tema es una hoja del subconjunto CSS de [`crate::export_css`] empotrada +//! con `include_str!`, así que no hay nada que instalar y una compilación local +//! se comporta igual que el paquete Flatpak. Los temas solo tocan tipografía y +//! color: la geometría de la página es cosa de la plantilla, de modo que +//! cualquier tema se combina con cualquier plantilla. + +use crate::export_css::{analizar, Ajustes}; +use crate::settings::ExportTheme; + +/// Hoja de estilo empotrada de un tema, si lo tiene. +pub fn hoja(tema: ExportTheme) -> Option<&'static str> { + match tema { + ExportTheme::None => None, + ExportTheme::Sepia => Some(include_str!("../data/themes/sepia.css")), + ExportTheme::HighContrast => Some(include_str!("../data/themes/high-contrast.css")), + ExportTheme::Technical => Some(include_str!("../data/themes/technical.css")), + ExportTheme::Academic => Some(include_str!("../data/themes/academic.css")), + } +} + +/// Ajustes resueltos de un tema incluido. +pub fn ajustes(tema: ExportTheme) -> Option { + hoja(tema).map(analizar) +} + +#[cfg(test)] +mod tests { + use super::*; + + const INCLUIDOS: [ExportTheme; 4] = [ + ExportTheme::Sepia, + ExportTheme::HighContrast, + ExportTheme::Technical, + ExportTheme::Academic, + ]; + + #[test] + fn el_tema_none_no_trae_hoja() { + assert!(hoja(ExportTheme::None).is_none()); + assert!(ajustes(ExportTheme::None).is_none()); + } + + #[test] + fn cada_tema_tiene_hoja_y_no_impone_margenes() { + for tema in INCLUIDOS { + let ajustes = ajustes(tema).expect("el tema debe tener hoja"); + assert!(!ajustes.es_vacio(), "{tema:?} no impone nada"); + assert_eq!( + ajustes.margenes, + Default::default(), + "{tema:?} no debe tocar los márgenes" + ); + } + } + + #[test] + fn las_hojas_incluidas_no_dejan_propiedades_sin_efecto() { + for tema in INCLUIDOS { + let ajustes = ajustes(tema).expect("el tema debe tener hoja"); + assert!( + ajustes.ignoradas.is_empty(), + "{tema:?} tiene propiedades sin efecto: {:?}", + ajustes.ignoradas + ); + } + } + + #[test] + fn cada_tema_fija_tipografia_y_colores() { + for tema in INCLUIDOS { + let ajustes = ajustes(tema).expect("el tema debe tener hoja"); + assert!(ajustes.familia.is_some(), "{tema:?} sin familia"); + assert!(ajustes.tamano_cuerpo.is_some(), "{tema:?} sin tamaño"); + assert!(ajustes.interlinea.is_some(), "{tema:?} sin interlineado"); + assert!(ajustes.colores.texto.is_some(), "{tema:?} sin color"); + assert!(ajustes.colores.fondo.is_some(), "{tema:?} sin fondo"); + assert!( + ajustes.colores.atenuado.is_some(), + "{tema:?} sin color atenuado" + ); + } + } + + #[test] + fn los_temas_se_distinguen_entre_si() { + let mut vistos: Vec = Vec::new(); + for tema in INCLUIDOS { + let ajustes = ajustes(tema).expect("el tema debe tener hoja"); + assert!( + !vistos.contains(&ajustes), + "{tema:?} repite los ajustes de otro tema" + ); + vistos.push(ajustes); + } + } +} diff --git a/src/main.rs b/src/main.rs index 0fa23d6..cefbdfb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,6 +13,7 @@ mod arranque; mod editor; mod export_css; mod export_pdf; +mod export_themes; mod file_manager; mod guardado; mod markdown_render; diff --git a/src/preferences.rs b/src/preferences.rs index afe3b01..289f2d8 100644 --- a/src/preferences.rs +++ b/src/preferences.rs @@ -10,7 +10,7 @@ use std::rc::Rc; use crate::settings::{ gtk_hides_invisible_safely, AppSettings, ExportColorScheme, ExportMonoFont, ExportOrientation, - ExportPageSize, ExportTemplate, FontFamily, MarkupVisibility, + ExportPageSize, ExportTemplate, ExportTheme, FontFamily, MarkupVisibility, }; fn spin(title: &str, subtitle: &str, adj: >k4::Adjustment, digits: u32) -> adw::SpinRow { @@ -395,6 +395,8 @@ fn editor_page(settings: &Rc, apply: &Rc) -> adw::Prefere } /// Descripción de la hoja de estilo elegida para la fila de preferencias. +/// Texto de la fila de hoja de estilo: la ruta elegida o el aviso de que no +/// hay ninguna. fn describir_hoja(settings: &AppSettings) -> String { match settings.export_css_path() { Some(ruta) => ruta.display().to_string(), @@ -483,6 +485,18 @@ fn export_page(settings: &Rc, apply: &Rc) -> adw::Prefere settings.export_orientation().index(), ); + let tema = combo_i18n( + &gettext("PDF theme"), + &gettext("Typography and colours on top of the template"), + &[ + gettext("None"), + gettext("Sepia"), + gettext("High contrast"), + gettext("Technical"), + gettext("Academic"), + ], + settings.export_theme().index(), + ); let estilos = adw::ActionRow::builder() .title(gettext("Stylesheet")) .subtitle(describir_hoja(settings)) @@ -512,13 +526,28 @@ fn export_page(settings: &Rc, apply: &Rc) -> adw::Prefere } group.add(&template); - for row in [&scheme, &family, &mono, &page_size, &orientation] { + for row in [ + page_size.upcast_ref::(), + orientation.upcast_ref::(), + scheme.upcast_ref::(), + family.upcast_ref::(), + size.upcast_ref::(), + mono.upcast_ref::(), + ] { group.add(row); } - group.add(&size); - group.add(&estilos); page.add(&group); + let grupo_color = adw::PreferencesGroup::builder() + .title(gettext("Typography and colour")) + .description(gettext( + "A theme changes type and colours on top of the template. Your own stylesheet is applied last, so it wins over both", + )) + .build(); + grupo_color.add(&tema); + grupo_color.add(&estilos); + page.add(&grupo_color); + { let settings = settings.clone(); let apply = apply.clone(); @@ -580,6 +609,14 @@ fn export_page(settings: &Rc, apply: &Rc) -> adw::Prefere apply(); }); } + { + let settings = settings.clone(); + let apply = apply.clone(); + tema.connect_selected_notify(move |row| { + settings.set_export_theme(ExportTheme::from_index(row.selected())); + apply(); + }); + } { let settings = settings.clone(); let apply = apply.clone(); @@ -601,8 +638,12 @@ fn export_page(settings: &Rc, apply: &Rc) -> adw::Prefere let estilos = estilos.clone(); let quitar = quitar.clone(); dialog.open(ventana.as_ref(), gio::Cancellable::NONE, move |resultado| { - let Ok(fichero) = resultado else { - return; + let fichero = match resultado { + Ok(fichero) => fichero, + Err(error) => { + eprintln!("No se pudo elegir la hoja de estilo: {error}"); + return; + } }; let Some(ruta) = fichero.path() else { return; diff --git a/src/settings.rs b/src/settings.rs index e04aef4..09da0d0 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -285,6 +285,59 @@ impl ExportOrientation { } } +/// Tema de hoja de estilo aplicado encima de la plantilla de página. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ExportTheme { + /// Sin tema: la plantilla tal cual. + None, + Sepia, + HighContrast, + Technical, + Academic, +} + +impl ExportTheme { + fn from_nick(nick: &str) -> Self { + match nick { + "sepia" => Self::Sepia, + "high-contrast" => Self::HighContrast, + "technical" => Self::Technical, + "academic" => Self::Academic, + _ => Self::None, + } + } + + pub fn nick(self) -> &'static str { + match self { + Self::None => "none", + Self::Sepia => "sepia", + Self::HighContrast => "high-contrast", + Self::Technical => "technical", + Self::Academic => "academic", + } + } + + pub fn index(self) -> u32 { + match self { + Self::None => 0, + Self::Sepia => 1, + Self::HighContrast => 2, + Self::Technical => 3, + Self::Academic => 4, + } + } + + pub fn from_index(index: u32) -> Self { + match index { + 1 => Self::Sepia, + 2 => Self::HighContrast, + 3 => Self::Technical, + 4 => Self::Academic, + _ => Self::None, + } + } +} + /// Plantilla de página del PDF exportado. `Custom` usa los ajustes /// granulares; el resto fija sus propios valores de maquetación. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -665,6 +718,20 @@ impl AppSettings { }); } + /// Tema de hoja de estilo aplicado encima de la plantilla. + pub fn export_theme(&self) -> ExportTheme { + ExportTheme::from_nick( + &self.get("none".to_string(), |s| s.string("export-theme").to_string()), + ) + } + pub fn set_export_theme(&self, v: ExportTheme) { + self.set(|s| { + if let Err(e) = s.set_string("export-theme", v.nick()) { + log_set_error("export-theme", e); + } + }); + } + /// Ruta de la hoja de estilo del usuario. Vacía significa "ninguna". pub fn export_css_path(&self) -> Option { let ruta = self.get(String::new(), |s| s.string("export-css-path").to_string()); diff --git a/tests/export_pdf.rs b/tests/export_pdf.rs index fce6488..82e0770 100644 --- a/tests/export_pdf.rs +++ b/tests/export_pdf.rs @@ -12,6 +12,8 @@ mod export_css; #[path = "../src/export_pdf.rs"] mod export_pdf; +#[path = "../src/export_themes.rs"] +mod export_themes; #[path = "../src/markdown_render.rs"] mod markdown_render; #[path = "../src/settings.rs"] @@ -106,6 +108,7 @@ fn la_hoja_de_estilo_cambia_el_pdf_y_quitarla_lo_deja_igual() { papel: settings::ExportPageSize::A4, orientacion: settings::ExportOrientation::Portrait, template: settings::ExportTemplate::Custom, + theme: settings::ExportTheme::None, css_path: None, }; let con_hoja = export_pdf::ExportConfig { @@ -132,6 +135,50 @@ fn la_hoja_de_estilo_cambia_el_pdf_y_quitarla_lo_deja_igual() { eprintln!("PDFs de prueba en {}", dir.display()); } +#[test] +#[ignore = "requiere GTK y pdftoppm; ejecutar con xvfb-run y --ignored"] +fn un_tema_cambia_el_pdf_y_quitarlo_lo_deja_igual() { + gtk4::init().expect("GTK necesita un display; ejecuta bajo xvfb-run"); + libadwaita::init().expect("libadwaita init"); + + let dir = std::env::temp_dir().join("scribe-tema-prueba"); + std::fs::create_dir_all(&dir).expect("crear directorio de prueba"); + + let base = export_pdf::ExportConfig { + esquema: settings::ExportColorScheme::Light, + familia_cuerpo: settings::FontFamily::Sans, + tamano_cuerpo: 11.0, + fuente_mono: settings::ExportMonoFont::AdwaitaMono, + papel: settings::ExportPageSize::A4, + orientacion: settings::ExportOrientation::Portrait, + template: settings::ExportTemplate::Article, + theme: settings::ExportTheme::None, + css_path: None, + }; + let con_tema = export_pdf::ExportConfig { + theme: settings::ExportTheme::Sepia, + ..base.clone() + }; + + let sin = dir.join("sin-tema.pdf"); + let con = dir.join("con-tema.pdf"); + let otra_vez = dir.join("otra-vez.pdf"); + export_pdf::exportar_a_fichero(&base, DOC, "Prueba", &sin, false).expect("exportar sin tema"); + export_pdf::exportar_a_fichero(&con_tema, DOC, "Prueba", &con, false) + .expect("exportar con tema"); + export_pdf::exportar_a_fichero(&base, DOC, "Prueba", &otra_vez, false).expect("reexportar"); + + let Some(a) = raster(&sin, &dir.join("a")) else { + eprintln!("pdftoppm no disponible: se omite la comparación visual"); + return; + }; + let b = raster(&con, &dir.join("b")).expect("rasterizar el PDF con tema"); + let c = raster(&otra_vez, &dir.join("c")).expect("rasterizar el PDF sin tema"); + assert_eq!(a, c, "quitar el tema debe dejar la página como estaba"); + assert_ne!(a, b, "el tema debe cambiar la página"); + eprintln!("PDFs de prueba en {}", dir.display()); +} + #[test] #[ignore = "requiere display: xvfb-run -a cargo test --test export_pdf -- --ignored --nocapture"] fn exporta_un_pdf_real_de_prueba() { @@ -148,6 +195,7 @@ fn exporta_un_pdf_real_de_prueba() { papel: settings::ExportPageSize::A4, orientacion: settings::ExportOrientation::Portrait, template: settings::ExportTemplate::Custom, + theme: settings::ExportTheme::None, css_path: None, }; export_pdf::exportar_a_fichero(&config, DOC, "Prueba", &ruta, false).expect("exportar a PDF"); diff --git a/tests/tabs_smoke.rs b/tests/tabs_smoke.rs index 319d29d..5f5cf2e 100644 --- a/tests/tabs_smoke.rs +++ b/tests/tabs_smoke.rs @@ -20,6 +20,8 @@ mod editor; mod export_css; #[path = "../src/export_pdf.rs"] mod export_pdf; +#[path = "../src/export_themes.rs"] +mod export_themes; #[path = "../src/file_manager.rs"] mod file_manager; #[path = "../src/guardado.rs"]