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"]