summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJaakko Keränen <jaakko.keranen@iki.fi>2021-10-11 12:22:54 +0300
committerJaakko Keränen <jaakko.keranen@iki.fi>2021-10-11 12:22:54 +0300
commit960df03c17091aca37f53eaab8fc27c669d26a5e (patch)
tree18721567f227928c528430daf9d06248a2443598
parentbf9158b29df506698e267d4583459ee4ebf20685 (diff)
Media refactoring; working on FontPack management
Media still needs more work to get rid of redundancies and make lookups faster. FontPacks are manipulated as Media items (not unlike images) so they can be previewed on page, and installed via a click. FontPack management is not trivial as it includes such details as versioning and whether individual packs are enabled or disabled.
-rw-r--r--res/arabic.fontpack/fontpack.ini2
-rw-r--r--res/cjk.fontpack/fontpack.ini2
-rw-r--r--res/default.fontpack/fontpack.ini2
-rw-r--r--res/firasans.fontpack/fontpack.ini2
-rw-r--r--res/literata.fontpack/fontpack.ini2
-rw-r--r--res/nunito.fontpack/fontpack.ini2
-rw-r--r--res/tinos.fontpack/fontpack.ini2
-rw-r--r--src/app.c1
-rw-r--r--src/defs.h4
-rw-r--r--src/fontpack.c299
-rw-r--r--src/fontpack.h65
-rw-r--r--src/gempub.c2
-rw-r--r--src/gmdocument.c159
-rw-r--r--src/gmdocument.h16
-rw-r--r--src/gmrequest.c9
-rw-r--r--src/gmutil.c8
-rw-r--r--src/media.c349
-rw-r--r--src/media.h92
-rw-r--r--src/ui/documentwidget.c429
-rw-r--r--src/ui/documentwidget.h2
-rw-r--r--src/ui/mediaui.c88
-rw-r--r--src/ui/mediaui.h20
22 files changed, 1030 insertions, 527 deletions
diff --git a/res/arabic.fontpack/fontpack.ini b/res/arabic.fontpack/fontpack.ini
index 00ad5241..305878ce 100644
--- a/res/arabic.fontpack/fontpack.ini
+++ b/res/arabic.fontpack/fontpack.ini
@@ -1,3 +1,5 @@
1version = 1
2
1[arabic] 3[arabic]
2name = "Noto Sans Arabic UI" 4name = "Noto Sans Arabic UI"
3auxiliary = true 5auxiliary = true
diff --git a/res/cjk.fontpack/fontpack.ini b/res/cjk.fontpack/fontpack.ini
index fbac54be..6e0d274c 100644
--- a/res/cjk.fontpack/fontpack.ini
+++ b/res/cjk.fontpack/fontpack.ini
@@ -1,3 +1,5 @@
1version = 1
2
1[notosansjp] 3[notosansjp]
2name = "Noto Sans JP" 4name = "Noto Sans JP"
3auxiliary = true 5auxiliary = true
diff --git a/res/default.fontpack/fontpack.ini b/res/default.fontpack/fontpack.ini
index 68316ef6..f8ef31ce 100644
--- a/res/default.fontpack/fontpack.ini
+++ b/res/default.fontpack/fontpack.ini
@@ -17,6 +17,8 @@
17# `glyphscale` and `voffset` can also be specified separately for the UI and 17# `glyphscale` and `voffset` can also be specified separately for the UI and
18# document domains by prefixing `ui.` or `doc.` to the key. 18# document domains by prefixing `ui.` or `doc.` to the key.
19 19
20version = 1
21
20[default] 22[default]
21name = "Source Sans" 23name = "Source Sans"
22regular = "SourceSans3-Regular.ttf" 24regular = "SourceSans3-Regular.ttf"
diff --git a/res/firasans.fontpack/fontpack.ini b/res/firasans.fontpack/fontpack.ini
index 4378a757..c6eb4c77 100644
--- a/res/firasans.fontpack/fontpack.ini
+++ b/res/firasans.fontpack/fontpack.ini
@@ -1,3 +1,5 @@
1version = 1
2
1[firasans] 3[firasans]
2name = "Fira Sans" 4name = "Fira Sans"
3glyphscale = 0.85 5glyphscale = 0.85
diff --git a/res/literata.fontpack/fontpack.ini b/res/literata.fontpack/fontpack.ini
index e4e49bcb..7c29491d 100644
--- a/res/literata.fontpack/fontpack.ini
+++ b/res/literata.fontpack/fontpack.ini
@@ -1,3 +1,5 @@
1version = 1
2
1[literata] 3[literata]
2name = "Literata" 4name = "Literata"
3regular = "Literata-Regular-opsz=14.ttf" 5regular = "Literata-Regular-opsz=14.ttf"
diff --git a/res/nunito.fontpack/fontpack.ini b/res/nunito.fontpack/fontpack.ini
index ea4a12b8..2f2471e1 100644
--- a/res/nunito.fontpack/fontpack.ini
+++ b/res/nunito.fontpack/fontpack.ini
@@ -1,3 +1,5 @@
1version = 1
2
1[nunito] 3[nunito]
2name = "Nunito" 4name = "Nunito"
3tweaks = 0x1 # some hardcoded kerning changes (`Th`, etc.) 5tweaks = 0x1 # some hardcoded kerning changes (`Th`, etc.)
diff --git a/res/tinos.fontpack/fontpack.ini b/res/tinos.fontpack/fontpack.ini
index 8759b752..a2cf811e 100644
--- a/res/tinos.fontpack/fontpack.ini
+++ b/res/tinos.fontpack/fontpack.ini
@@ -1,3 +1,5 @@
1version = 1
2
1[tinos] 3[tinos]
2name = "Tinos" 4name = "Tinos"
3glyphscale = 0.850 5glyphscale = 0.850
diff --git a/src/app.c b/src/app.c
index b317e7b3..cb5479e8 100644
--- a/src/app.c
+++ b/src/app.c
@@ -2669,6 +2669,7 @@ iBool handleCommand_App(const char *cmd) {
2669 const iBool isSplit = numRoots_Window(get_Window()) > 1; 2669 const iBool isSplit = numRoots_Window(get_Window()) > 1;
2670 if (tabCount_Widget(tabs) > 1 || isSplit) { 2670 if (tabCount_Widget(tabs) > 1 || isSplit) {
2671 iWidget *closed = removeTabPage_Widget(tabs, index); 2671 iWidget *closed = removeTabPage_Widget(tabs, index);
2672 cancelAllRequests_DocumentWidget((iDocumentWidget *) closed);
2672 destroy_Widget(closed); /* released later */ 2673 destroy_Widget(closed); /* released later */
2673 if (index == tabCount_Widget(tabs)) { 2674 if (index == tabCount_Widget(tabs)) {
2674 index--; 2675 index--;
diff --git a/src/defs.h b/src/defs.h
index 65096389..f5479cf3 100644
--- a/src/defs.h
+++ b/src/defs.h
@@ -125,11 +125,13 @@ iLocalDef int acceptKeyMod_ReturnKeyBehavior(int behavior) {
125#define delete_Icon "\u232b" 125#define delete_Icon "\u232b"
126#define copy_Icon "\u2398" //"\u2bba" 126#define copy_Icon "\u2398" //"\u2bba"
127#define check_Icon "\u2714" 127#define check_Icon "\u2714"
128#define ballotCheck_Icon "\U0001f5f9" 128#define ballotChecked_Icon "\U0001f5f9"
129#define ballotUnchecked_Icon "\U0001f5f9"
129#define inbox_Icon "\U0001f4e5" 130#define inbox_Icon "\U0001f4e5"
130#define book_Icon "\U0001f56e" 131#define book_Icon "\U0001f56e"
131#define bookmark_Icon "\U0001f516" 132#define bookmark_Icon "\U0001f516"
132#define folder_Icon "\U0001f4c1" 133#define folder_Icon "\U0001f4c1"
134#define file_Icon "\U0001f5ce"
133#define openTab_Icon "\u2750" 135#define openTab_Icon "\u2750"
134#define openTabBg_Icon "\u2b1a" 136#define openTabBg_Icon "\u2b1a"
135#define openExt_Icon "\u27a0" 137#define openExt_Icon "\u27a0"
diff --git a/src/fontpack.c b/src/fontpack.c
index ca1d1582..fb1c98ee 100644
--- a/src/fontpack.c
+++ b/src/fontpack.c
@@ -33,7 +33,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
33#include <the_Foundation/string.h> 33#include <the_Foundation/string.h>
34#include <the_Foundation/toml.h> 34#include <the_Foundation/toml.h>
35 35
36/* TODO: Clean up and/or reorder this file, it's a bit unorganized. */ 36const char *mimeType_FontPack = "application/lagrange-fontpack+zip";
37 37
38float scale_FontSize(enum iFontSize size) { 38float scale_FontSize(enum iFontSize size) {
39 static const float sizes[max_FontSize] = { 39 static const float sizes[max_FontSize] = {
@@ -57,38 +57,9 @@ float scale_FontSize(enum iFontSize size) {
57 return sizes[size]; 57 return sizes[size];
58} 58}
59 59
60iDeclareType(Fonts)
61
62struct Impl_Fonts {
63 iString userDir;
64 iPtrArray packs;
65 iPtrArray files;
66 iPtrArray specOrder; /* specs sorted by priority */
67};
68
69static iFonts fonts_;
70
71static void unloadFiles_Fonts_(iFonts *d) {
72 /* TODO: Mark all files in font packs as not resident. */
73 iForEach(PtrArray, i, &d->files) {
74 delete_FontFile(i.ptr);
75 }
76 clear_PtrArray(&d->files);
77}
78
79static iFontFile *findFile_Fonts_(iFonts *d, const iString *id) {
80 iForEach(PtrArray, i, &d->files) {
81 iFontFile *ff = i.ptr;
82 if (equal_String(&ff->id, id)) {
83 return ff;
84 }
85 }
86 return NULL;
87}
88
89/*----------------------------------------------------------------------------------------------*/ 60/*----------------------------------------------------------------------------------------------*/
90 61
91iDefineTypeConstruction(FontFile) 62iDefineObjectConstruction(FontFile)
92 63
93void init_FontFile(iFontFile *d) { 64void init_FontFile(iFontFile *d) {
94 init_String(&d->id); 65 init_String(&d->id);
@@ -114,7 +85,7 @@ static void load_FontFile_(iFontFile *d, const iBlock *data) {
114 HB_MEMORY_MODE_READONLY, NULL, NULL); 85 HB_MEMORY_MODE_READONLY, NULL, NULL);
115 d->hbFace = hb_face_create(d->hbBlob, 0); 86 d->hbFace = hb_face_create(d->hbBlob, 0);
116 d->hbFont = hb_font_create(d->hbFace); 87 d->hbFont = hb_font_create(d->hbFace);
117#endif 88#endif
118} 89}
119 90
120static void unload_FontFile_(iFontFile *d) { 91static void unload_FontFile_(iFontFile *d) {
@@ -126,12 +97,13 @@ static void unload_FontFile_(iFontFile *d) {
126 d->hbFont = NULL; 97 d->hbFont = NULL;
127 d->hbFace = NULL; 98 d->hbFace = NULL;
128 d->hbBlob = NULL; 99 d->hbBlob = NULL;
129#endif 100#endif
130 clear_Block(&d->sourceData); 101 clear_Block(&d->sourceData);
131 iZap(d->stbInfo); 102 iZap(d->stbInfo);
132} 103}
133 104
134void deinit_FontFile(iFontFile *d) { 105void deinit_FontFile(iFontFile *d) {
106 printf("FontFile %p {%s} is DESTROYED\n", d, cstr_String(&d->id));
135 unload_FontFile_(d); 107 unload_FontFile_(d);
136 deinit_Block(&d->sourceData); 108 deinit_Block(&d->sourceData);
137 deinit_String(&d->id); 109 deinit_String(&d->id);
@@ -156,12 +128,12 @@ void measureGlyph_FontFile(const iFontFile *d, uint32_t glyphIndex,
156 128
157/*----------------------------------------------------------------------------------------------*/ 129/*----------------------------------------------------------------------------------------------*/
158 130
159
160iDefineTypeConstruction(FontSpec) 131iDefineTypeConstruction(FontSpec)
161 132
162void init_FontSpec(iFontSpec *d) { 133void init_FontSpec(iFontSpec *d) {
163 init_String(&d->id); 134 init_String(&d->id);
164 init_String(&d->name); 135 init_String(&d->name);
136 init_String(&d->sourcePath);
165 d->flags = 0; 137 d->flags = 0;
166 d->priority = 0; 138 d->priority = 0;
167 for (int i = 0; i < 2; ++i) { 139 for (int i = 0; i < 2; ++i) {
@@ -173,52 +145,119 @@ void init_FontSpec(iFontSpec *d) {
173} 145}
174 146
175void deinit_FontSpec(iFontSpec *d) { 147void deinit_FontSpec(iFontSpec *d) {
148 /* FontFile references are held by FontSpecs. */
149 iForIndices(i, d->styles) {
150 iRelease(d->styles[i]);
151 }
152 deinit_String(&d->sourcePath);
176 deinit_String(&d->name); 153 deinit_String(&d->name);
177 deinit_String(&d->id); 154 deinit_String(&d->id);
178} 155}
179 156
180/*----------------------------------------------------------------------------------------------*/ 157/*----------------------------------------------------------------------------------------------*/
181 158
182iDeclareType(FontPack) 159iDeclareType(Fonts)
183iDeclareTypeConstruction(FontPack) 160
161struct Impl_Fonts {
162 iString userDir;
163 iPtrArray packs;
164 iObjectList *files;
165 iPtrArray specOrder; /* specs sorted by priority */
166};
167
168static iFonts fonts_;
169
170static void unloadFiles_Fonts_(iFonts *d) {
171 /* TODO: Mark all files in font packs as not resident. */
172 clear_ObjectList(d->files);
173}
174
175static iFontFile *findFile_Fonts_(iFonts *d, const iString *id) {
176 iForEach(ObjectList, i, d->files) {
177 iFontFile *ff = i.object;
178 if (equal_String(&ff->id, id)) {
179 return ff;
180 }
181 }
182 return NULL;
183}
184
185static void releaseUnusedFiles_Fonts_(iFonts *d) {
186 iForEach(ObjectList, i, d->files) {
187 iFontFile *ff = i.object;
188 if (ff->object.refCount == 1) {
189 /* No specs use this. */
190 //printf("[Fonts] releasing unused font file: %p {%s}\n", ff, cstr_String(&ff->id));
191 remove_ObjectListIterator(&i);
192 }
193 }
194}
195
196/*----------------------------------------------------------------------------------------------*/
184 197
185struct Impl_FontPack { 198struct Impl_FontPack {
186 const iArchive *archive; /* opened ZIP archive */ 199 iString id; /* lowercase filename without the .fontpack extension */
200 int version;
201 iBool isStandalone;
202 iBool isReadOnly;
187 iArray fonts; /* array of FontSpecs */ 203 iArray fonts; /* array of FontSpecs */
204 const iArchive *archive; /* opened ZIP archive */
188 iString * loadPath; 205 iString * loadPath;
189 iFontSpec * loadSpec; 206 iFontSpec * loadSpec;
190}; 207};
191 208
209iDefineTypeConstruction(FontPack)
210
192void init_FontPack(iFontPack *d) { 211void init_FontPack(iFontPack *d) {
193 d->archive = NULL; 212 init_String(&d->id);
213 d->version = 0;
214 d->isStandalone = iFalse;
215 d->isReadOnly = iFalse;
194 init_Array(&d->fonts, sizeof(iFontSpec)); 216 init_Array(&d->fonts, sizeof(iFontSpec));
217 d->archive = NULL;
195 d->loadSpec = NULL; 218 d->loadSpec = NULL;
196 d->loadPath = NULL; 219 d->loadPath = NULL;
197} 220}
198 221
199void deinit_FontPack(iFontPack *d) { 222void deinit_FontPack(iFontPack *d) {
223 iAssert(d->archive == NULL);
224 iAssert(d->loadSpec == NULL);
200 delete_String(d->loadPath); 225 delete_String(d->loadPath);
201 iForEach(Array, i, &d->fonts) { 226 iForEach(Array, i, &d->fonts) {
202 deinit_FontSpec(i.value); 227 deinit_FontSpec(i.value);
203 } 228 }
204 deinit_Array(&d->fonts); 229 deinit_Array(&d->fonts);
205 iAssert(d->archive == NULL); 230 deinit_String(&d->id);
206 iAssert(d->loadSpec == NULL); 231 releaseUnusedFiles_Fonts_(&fonts_);
207} 232}
208 233
209iDefineTypeConstruction(FontPack) 234iFontPackId id_FontPack(const iFontPack *d) {
235 return (iFontPackId){ &d->id, d->version };
236}
237
238const iPtrArray *listSpecs_FontPack(const iFontPack *d) {
239 if (!d) return NULL;
240 iPtrArray *list = collectNew_PtrArray();
241 iConstForEach(Array, i, &d->fonts) {
242 pushBack_PtrArray(list, i.value);
243 }
244 return list;
245}
210 246
211void handleIniTable_FontPack_(void *context, const iString *table, iBool isStart) { 247void handleIniTable_FontPack_(void *context, const iString *table, iBool isStart) {
212 iFontPack *d = context; 248 iFontPack *d = context;
213 if (isStart) { 249 if (isStart) {
214 iAssert(!d->loadSpec); 250 iAssert(!d->loadSpec);
215 /* Each font ID must be unique. */ 251 /* Each font ID must be unique in the non-standalone packs. */
216 if (!findSpec_Fonts(cstr_String(table))) { 252 if (d->isStandalone || !findSpec_Fonts(cstr_String(table))) {
217 d->loadSpec = new_FontSpec(); 253 d->loadSpec = new_FontSpec();
218 set_String(&d->loadSpec->id, table); 254 set_String(&d->loadSpec->id, table);
255 if (d->loadPath) {
256 set_String(&d->loadSpec->sourcePath, d->loadPath);
257 }
219 } 258 }
220 } 259 }
221 else { 260 else if (d->loadSpec) {
222 /* Set fallback font files. */ { 261 /* Set fallback font files. */ {
223 const iFontFile **styles = d->loadSpec->styles; 262 const iFontFile **styles = d->loadSpec->styles;
224 if (!styles[regular_FontStyle]) { 263 if (!styles[regular_FontStyle]) {
@@ -229,11 +268,11 @@ void handleIniTable_FontPack_(void *context, const iString *table, iBool isStart
229 return; 268 return;
230 } 269 }
231 if (!styles[semiBold_FontStyle]) { 270 if (!styles[semiBold_FontStyle]) {
232 styles[semiBold_FontStyle] = styles[bold_FontStyle]; 271 styles[semiBold_FontStyle] = ref_Object(styles[bold_FontStyle]);
233 } 272 }
234 for (size_t s = 0; s < max_FontStyle; s++) { 273 for (size_t s = 0; s < max_FontStyle; s++) {
235 if (!styles[s]) { 274 if (!styles[s]) {
236 styles[s] = styles[regular_FontStyle]; 275 styles[s] = ref_Object(styles[regular_FontStyle]);
237 } 276 }
238 } 277 }
239 } 278 }
@@ -263,7 +302,15 @@ static iBlock *readFile_FontPack_(const iFontPack *d, const iString *path) {
263void handleIniKeyValue_FontPack_(void *context, const iString *table, const iString *key, 302void handleIniKeyValue_FontPack_(void *context, const iString *table, const iString *key,
264 const iTomlValue *value) { 303 const iTomlValue *value) {
265 iFontPack *d = context; 304 iFontPack *d = context;
266 if (!d->loadSpec) return; 305 if (isEmpty_String(table)) {
306 if (!cmp_String(key, "version")) {
307 d->version = number_TomlValue(value);
308 }
309 return;
310 }
311 if (!d->loadSpec) {
312 return;
313 }
267 iUnused(table); 314 iUnused(table);
268 if (!cmp_String(key, "name") && value->type == string_TomlType) { 315 if (!cmp_String(key, "name") && value->type == string_TomlType) {
269 set_String(&d->loadSpec->name, value->value.string); 316 set_String(&d->loadSpec->name, value->value.string);
@@ -322,12 +369,13 @@ void handleIniKeyValue_FontPack_(void *context, const iString *table, const iStr
322 ff = new_FontFile(); 369 ff = new_FontFile();
323 set_String(&ff->id, fontFileId); 370 set_String(&ff->id, fontFileId);
324 load_FontFile_(ff, data); 371 load_FontFile_(ff, data);
325 pushBack_PtrArray(&fonts_.files, ff); /* centralized ownership */ 372 pushBack_ObjectList(fonts_.files, ff); /* centralized ownership */
373 iRelease(ff);
326 delete_Block(data); 374 delete_Block(data);
327// printf("[FontPack] loaded file: %s\n", cstr_String(fontFileId)); 375// printf("[FontPack] loaded file: %s\n", cstr_String(fontFileId));
328 } 376 }
329 } 377 }
330 d->loadSpec->styles[i] = ff; 378 d->loadSpec->styles[i] = ref_Object(ff);
331 delete_String(fontFileId); 379 delete_String(fontFileId);
332 break; 380 break;
333 } 381 }
@@ -348,29 +396,6 @@ static iBool load_FontPack_(iFontPack *d, const iString *ini) {
348 return ok; 396 return ok;
349} 397}
350 398
351#if 0
352iBool loadIniFile_FontPack(iFontPack *d, const iString *iniPath) {
353 iBeginCollect();
354 iBool ok = iFalse;
355 iFile *f = iClob(new_File(iniPath));
356 if (open_File(f, text_FileMode | readOnly_FileMode)) {
357 d->loadPath = collect_String(newRange_String(dirName_Path(iniPath)));
358 iString *src = collect_String(readString_File(f));
359
360 iTomlParser *ini = collect_TomlParser(new_TomlParser());
361 setHandlers_TomlParser(ini, handleIniTable_FontPack_, handleIniKeyValue_FontPack_, d);
362 if (!parse_TomlParser(ini, src)) {
363 fprintf(stderr, "[FontPack] error parsing %s\n", cstr_String(iniPath));
364 }
365 iAssert(d->loadSpec == NULL);
366 d->loadPath = NULL;
367 ok = iTrue;
368 }
369 iEndCollect();
370 return ok;
371}
372#endif
373
374iBool loadArchive_FontPack(iFontPack *d, const iArchive *zip) { 399iBool loadArchive_FontPack(iFontPack *d, const iArchive *zip) {
375 d->archive = zip; 400 d->archive = zip;
376 iBool ok = iFalse; 401 iBool ok = iFalse;
@@ -387,6 +412,28 @@ iBool loadArchive_FontPack(iFontPack *d, const iArchive *zip) {
387 return ok; 412 return ok;
388} 413}
389 414
415void setLoadPath_FontPack(iFontPack *d, const iString *path) {
416 if (!d->loadPath) {
417 d->loadPath = new_String();
418 }
419 set_String(d->loadPath, path);
420 /* Pack ID is based on the file name. */
421 setRange_String(&d->id, baseName_Path(path));
422 setRange_String(&d->id, withoutExtension_Path(&d->id));
423}
424
425void setStandalone_FontPack(iFontPack *d, iBool standalone) {
426 d->isStandalone = standalone;
427}
428
429void setReadOnly_FontPack(iFontPack *d, iBool readOnly) {
430 d->isReadOnly = readOnly;
431}
432
433iBool isReadOnly_FontPack(const iFontPack *d) {
434 return d->isReadOnly;
435}
436
390/*----------------------------------------------------------------------------------------------*/ 437/*----------------------------------------------------------------------------------------------*/
391 438
392static void unloadFonts_Fonts_(iFonts *d) { 439static void unloadFonts_Fonts_(iFonts *d) {
@@ -397,14 +444,23 @@ static void unloadFonts_Fonts_(iFonts *d) {
397 clear_PtrArray(&d->packs); 444 clear_PtrArray(&d->packs);
398} 445}
399 446
447static int cmpName_FontSpecPtr_(const void *a, const void *b) {
448 const iFontSpec **p1 = (const iFontSpec **) a, **p2 = (const iFontSpec **) b;
449 return cmpStringCase_String(&(*p1)->name, &(*p2)->name);
450}
451
400static int cmpPriority_FontSpecPtr_(const void *a, const void *b) { 452static int cmpPriority_FontSpecPtr_(const void *a, const void *b) {
401 const iFontSpec **p1 = (const iFontSpec **) a, **p2 = (const iFontSpec **) b; 453 const iFontSpec **p1 = (const iFontSpec **) a, **p2 = (const iFontSpec **) b;
402 return -iCmp((*p1)->priority, (*p2)->priority); /* highest priority first */ 454 const int cmp = -iCmp((*p1)->priority, (*p2)->priority); /* highest priority first */
455 if (cmp) return cmp;
456 return cmpName_FontSpecPtr_(a, b);
403} 457}
404 458
405static int cmpName_FontSpecPtr_(const void *a, const void *b) { 459static int cmpSourceAndPriority_FontSpecPtr_(const void *a, const void *b) {
406 const iFontSpec **p1 = (const iFontSpec **) a, **p2 = (const iFontSpec **) b; 460 const iFontSpec **p1 = (const iFontSpec **) a, **p2 = (const iFontSpec **) b;
407 return cmpStringCase_String(&(*p1)->name, &(*p2)->name); 461 const int cmp = cmpStringCase_String(&(*p1)->sourcePath, &(*p2)->sourcePath);
462 if (cmp) return cmp;
463 return cmpPriority_FontSpecPtr_(a, b);
408} 464}
409 465
410static void sortSpecs_Fonts_(iFonts *d) { 466static void sortSpecs_Fonts_(iFonts *d) {
@@ -422,11 +478,13 @@ void init_Fonts(const char *userDir) {
422 iFonts *d = &fonts_; 478 iFonts *d = &fonts_;
423 initCStr_String(&d->userDir, userDir); 479 initCStr_String(&d->userDir, userDir);
424 init_PtrArray(&d->packs); 480 init_PtrArray(&d->packs);
425 init_PtrArray(&d->files); 481 d->files = new_ObjectList();
426 init_PtrArray(&d->specOrder); 482 init_PtrArray(&d->specOrder);
427 /* Load the required fonts. */ { 483 /* Load the required fonts. */ {
428 iFontPack *pack = new_FontPack(); 484 iFontPack *pack = new_FontPack();
485 setCStr_String(&pack->id, "default");
429 iArchive *arch = new_Archive(); 486 iArchive *arch = new_Archive();
487 setReadOnly_FontPack(pack, iTrue);
430 openData_Archive(arch, &fontpackDefault_Embedded); 488 openData_Archive(arch, &fontpackDefault_Embedded);
431 loadArchive_FontPack(pack, arch); /* should never fail if we've made it this far */ 489 loadArchive_FontPack(pack, arch); /* should never fail if we've made it this far */
432 iRelease(arch); 490 iRelease(arch);
@@ -436,7 +494,7 @@ void init_Fonts(const char *userDir) {
436 const char *locations[] = { 494 const char *locations[] = {
437 ".", 495 ".",
438 "./fonts", 496 "./fonts",
439 "../share/lagrange", 497 "../share/lagrange", /* Note: These must match CMakeLists.txt install destination */
440 "../../share/lagrange", 498 "../../share/lagrange",
441 concatPath_CStr(userDir, "fonts"), 499 concatPath_CStr(userDir, "fonts"),
442 userDir, 500 userDir,
@@ -450,7 +508,13 @@ void init_Fonts(const char *userDir) {
450 iArchive *arch = new_Archive(); 508 iArchive *arch = new_Archive();
451 if (openFile_Archive(arch, entryPath)) { 509 if (openFile_Archive(arch, entryPath)) {
452 iFontPack *pack = new_FontPack(); 510 iFontPack *pack = new_FontPack();
453 pack->loadPath = copy_String(entryPath); 511 setLoadPath_FontPack(pack, entryPath);
512 setReadOnly_FontPack(pack, !isWritable_FileInfo(entry.value));
513#if defined (iPlatformApple)
514 if (startsWith_String(pack->loadPath, cstr_String(execDir))) {
515 setReadOnly_FontPack(pack, iTrue);
516 }
517#endif
454 if (loadArchive_FontPack(pack, arch)) { 518 if (loadArchive_FontPack(pack, arch)) {
455 pushBack_PtrArray(&d->packs, pack); 519 pushBack_PtrArray(&d->packs, pack);
456 } 520 }
@@ -491,13 +555,18 @@ void init_Fonts(const char *userDir) {
491void deinit_Fonts(void) { 555void deinit_Fonts(void) {
492 iFonts *d = &fonts_; 556 iFonts *d = &fonts_;
493 unloadFonts_Fonts_(d); 557 unloadFonts_Fonts_(d);
494 unloadFiles_Fonts_(d); 558 //unloadFiles_Fonts_(d);
559 iAssert(isEmpty_ObjectList(d->files));
495 deinit_PtrArray(&d->specOrder); 560 deinit_PtrArray(&d->specOrder);
496 deinit_PtrArray(&d->packs); 561 deinit_PtrArray(&d->packs);
497 deinit_PtrArray(&d->files); 562 iRelease(d->files);
498 deinit_String(&d->userDir); 563 deinit_String(&d->userDir);
499} 564}
500 565
566const iPtrArray *listPacks_Fonts(void) {
567 return &fonts_.packs;
568}
569
501const iFontSpec *findSpec_Fonts(const char *fontId) { 570const iFontSpec *findSpec_Fonts(const char *fontId) {
502 iFonts *d = &fonts_; 571 iFonts *d = &fonts_;
503 iConstForEach(PtrArray, i, &d->specOrder) { 572 iConstForEach(PtrArray, i, &d->specOrder) {
@@ -524,3 +593,73 @@ const iPtrArray *listSpecs_Fonts(iBool (*filterFunc)(const iFontSpec *)) {
524const iPtrArray *listSpecsByPriority_Fonts(void) { 593const iPtrArray *listSpecsByPriority_Fonts(void) {
525 return &fonts_.specOrder; 594 return &fonts_.specOrder;
526} 595}
596
597const iString *infoPage_Fonts(void) {
598 iFonts *d = &fonts_;
599 iString *str = collectNewCStr_String("# Fonts\n");
600 iPtrArray *specsByPack = collectNew_PtrArray();
601 setCopy_PtrArray(specsByPack, &d->specOrder);
602 sort_Array(specsByPack, cmpSourceAndPriority_FontSpecPtr_);
603 iString *currentSourcePath = collectNew_String();
604 iConstForEach(PtrArray, i, specsByPack) {
605 const iFontSpec *spec = i.ptr;
606 if (isEmpty_String(&spec->sourcePath)) {
607 continue; /* built-in font */
608 }
609 if (!equal_String(&spec->sourcePath, currentSourcePath)) {
610 appendFormat_String(str, "=> %s %s%s\n",
611 cstrCollect_String(makeFileUrl_String(&spec->sourcePath)),
612 endsWithCase_String(&spec->sourcePath, ".fontpack") ? "\U0001f520 " : "",
613 cstr_Rangecc(baseName_Path(&spec->sourcePath)));
614 set_String(currentSourcePath, &spec->sourcePath);
615 }
616 }
617 return str;
618}
619
620const iFontPack *findPack_Fonts(const iString *path) {
621 iFonts *d = &fonts_;
622 iConstForEach(PtrArray, i, &d->packs) {
623 const iFontPack *pack = i.ptr;
624 if (pack->loadPath && equal_String(pack->loadPath, path)) {
625 return pack;
626 }
627 }
628 return NULL;
629}
630
631iBool preloadLocalFontpackForPreview_Fonts(iGmDocument *doc) {
632 iBool wasLoaded = iFalse;
633 for (size_t linkId = 1; ; linkId++) {
634 const iString *linkUrl = linkUrl_GmDocument(doc, linkId);
635 if (!linkUrl) {
636 break; /* ran out of links */
637 }
638 const int linkFlags = linkFlags_GmDocument(doc, linkId);
639 if (linkFlags & fontpackFileExtension_GmLinkFlag &&
640 scheme_GmLinkFlag(linkFlags) == file_GmLinkScheme) {
641 iMediaId linkMedia = findMediaForLink_Media(media_GmDocument(doc), linkId, fontpack_MediaType);
642 if (linkMedia.type) {
643 continue; /* got this one already */
644 }
645 iString *filePath = localFilePathFromUrl_String(linkUrl);
646 iFile *f = new_File(filePath);
647 if (open_File(f, readOnly_FileMode)) {
648 iBlock *fontPackArchiveData = readAll_File(f);
649 setUrl_Media(media_GmDocument(doc), linkId, fontpack_MediaType, linkUrl);
650 setData_Media(media_GmDocument(doc),
651 linkId,
652 collectNewCStr_String(mimeType_FontPack),
653 fontPackArchiveData,
654 0);
655 delete_Block(fontPackArchiveData);
656 wasLoaded = iTrue;
657 }
658 iRelease(f);
659 }
660 }
661 return wasLoaded;
662}
663
664iDefineClass(FontFile)
665
diff --git a/src/fontpack.h b/src/fontpack.h
index e59154e3..429afb5d 100644
--- a/src/fontpack.h
+++ b/src/fontpack.h
@@ -30,6 +30,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
30# include <hb.h> 30# include <hb.h>
31#endif 31#endif
32 32
33extern const char *mimeType_FontPack;
34
33/* Fontpacks are ZIP archives that contain a configuration file and one of more font 35/* Fontpacks are ZIP archives that contain a configuration file and one of more font
34files. The fontpack format is used instead of plain TTF/OTF because the text renderer 36files. The fontpack format is used instead of plain TTF/OTF because the text renderer
35uses additional metadata about each font. 37uses additional metadata about each font.
@@ -68,21 +70,13 @@ enum iFontStyle {
68 70
69float scale_FontSize (enum iFontSize size); 71float scale_FontSize (enum iFontSize size);
70 72
71iDeclareType(FontSpec) 73/*----------------------------------------------------------------------------------------------*/
72iDeclareTypeConstruction(FontSpec)
73 74
74enum iFontSpecFlags { 75iDeclareClass(FontFile)
75 override_FontSpecFlag = iBit(1), 76iDeclareObjectConstruction(FontFile)
76 monospace_FontSpecFlag = iBit(2), /* can be used in preformatted content */
77 auxiliary_FontSpecFlag = iBit(3), /* only used for looking up glyphs missing from other fonts */
78 arabic_FontSpecFlag = iBit(4),
79 fixNunitoKerning_FontSpecFlag = iBit(31), /* manual hardcoded kerning tweaks for Nunito */
80};
81
82iDeclareType(FontFile)
83iDeclareTypeConstruction(FontFile)
84 77
85struct Impl_FontFile { 78struct Impl_FontFile {
79 iObject object; /* reference-counted */
86 iString id; /* for detecting when the same file is used in many places */ 80 iString id; /* for detecting when the same file is used in many places */
87 enum iFontStyle style; 81 enum iFontStyle style;
88 iBlock sourceData; 82 iBlock sourceData;
@@ -107,9 +101,26 @@ uint8_t * rasterizeGlyph_FontFile(const iFontFile *, float xScale, float yScal
107void measureGlyph_FontFile (const iFontFile *, uint32_t glyphIndex, 101void measureGlyph_FontFile (const iFontFile *, uint32_t glyphIndex,
108 float xScale, float yScale, float xShift, 102 float xScale, float yScale, float xShift,
109 int *x0, int *y0, int *x1, int *y1); 103 int *x0, int *y0, int *x1, int *y1);
104
105/*----------------------------------------------------------------------------------------------*/
106
107/* FontSpec describes a typeface, combining multiple fonts into a group.
108 The user will be choosing FontSpecs instead of individual font files. */
109iDeclareType(FontSpec)
110iDeclareTypeConstruction(FontSpec)
111
112enum iFontSpecFlags {
113 override_FontSpecFlag = iBit(1),
114 monospace_FontSpecFlag = iBit(2), /* can be used in preformatted content */
115 auxiliary_FontSpecFlag = iBit(3), /* only used for looking up glyphs missing from other fonts */
116 arabic_FontSpecFlag = iBit(4),
117 fixNunitoKerning_FontSpecFlag = iBit(31), /* manual hardcoded kerning tweaks for Nunito */
118};
119
110struct Impl_FontSpec { 120struct Impl_FontSpec {
111 iString id; /* unique ID */ 121 iString id; /* unique ID */
112 iString name; /* human-readable label */ 122 iString name; /* human-readable label */
123 iString sourcePath; /* file where the path was loaded, could be a .fontpack */
113 int flags; 124 int flags;
114 int priority; 125 int priority;
115 float heightScale[2]; /* overall height scaling; ui, document */ 126 float heightScale[2]; /* overall height scaling; ui, document */
@@ -121,10 +132,38 @@ struct Impl_FontSpec {
121iLocalDef int scaleType_FontSpec(enum iFontSize sizeId) { 132iLocalDef int scaleType_FontSpec(enum iFontSize sizeId) {
122 return sizeId / contentRegular_FontSize; 133 return sizeId / contentRegular_FontSize;
123} 134}
124 135
136/*----------------------------------------------------------------------------------------------*/
137
138iDeclareType(FontPack)
139iDeclareTypeConstruction(FontPack)
140
141iDeclareType(FontPackId)
142
143struct Impl_FontPackId {
144 const iString *id;
145 int version;
146};
147
148void setReadOnly_FontPack (iFontPack *, iBool readOnly);
149void setStandalone_FontPack (iFontPack *, iBool standalone);
150void setLoadPath_FontPack (iFontPack *, const iString *path);
151iBool loadArchive_FontPack (iFontPack *, const iArchive *zip);
152
153iFontPackId id_FontPack (const iFontPack *);
154const iPtrArray * listSpecs_FontPack (const iFontPack *);
155iBool isReadOnly_FontPack (const iFontPack *);
156
157iDeclareType(GmDocument)
158
125void init_Fonts (const char *userDir); 159void init_Fonts (const char *userDir);
126void deinit_Fonts (void); 160void deinit_Fonts (void);
127 161
162const iFontPack * findPack_Fonts (const iString *path);
128const iFontSpec * findSpec_Fonts (const char *fontId); 163const iFontSpec * findSpec_Fonts (const char *fontId);
164const iPtrArray * listPacks_Fonts (void);
129const iPtrArray * listSpecs_Fonts (iBool (*filterFunc)(const iFontSpec *)); 165const iPtrArray * listSpecs_Fonts (iBool (*filterFunc)(const iFontSpec *));
130const iPtrArray * listSpecsByPriority_Fonts (void); 166const iPtrArray * listSpecsByPriority_Fonts (void);
167const iString * infoPage_Fonts (void);
168
169iBool preloadLocalFontpackForPreview_Fonts (iGmDocument *doc);
diff --git a/src/gempub.c b/src/gempub.c
index 23846414..952d72a1 100644
--- a/src/gempub.c
+++ b/src/gempub.c
@@ -337,7 +337,7 @@ iBool preloadCoverImage_Gempub(const iGempub *d, iGmDocument *doc) {
337 for (size_t linkId = 1; ; linkId++) { 337 for (size_t linkId = 1; ; linkId++) {
338 const iString *linkUrl = linkUrl_GmDocument(doc, linkId); 338 const iString *linkUrl = linkUrl_GmDocument(doc, linkId);
339 if (!linkUrl) break; 339 if (!linkUrl) break;
340 if (findLinkImage_Media(media_GmDocument(doc), linkId)) { 340 if (findLinkImage_Media(media_GmDocument(doc), linkId).type) {
341 continue; /* got this already */ 341 continue; /* got this already */
342 } 342 }
343 if (linkFlags_GmDocument(doc, linkId) & imageFileExtension_GmLinkFlag) { 343 if (linkFlags_GmDocument(doc, linkId) & imageFileExtension_GmLinkFlag) {
diff --git a/src/gmdocument.c b/src/gmdocument.c
index c98b0bb8..c271ad94 100644
--- a/src/gmdocument.c
+++ b/src/gmdocument.c
@@ -27,6 +27,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
27#include "ui/color.h" 27#include "ui/color.h"
28#include "ui/text.h" 28#include "ui/text.h"
29#include "ui/metrics.h" 29#include "ui/metrics.h"
30#include "ui/mediaui.h"
30#include "ui/window.h" 31#include "ui/window.h"
31#include "visited.h" 32#include "visited.h"
32#include "bookmarks.h" 33#include "bookmarks.h"
@@ -224,7 +225,7 @@ static iRangecc addLink_GmDocument_(iGmDocument *d, iRangecc line, iGmLinkId *li
224 setScheme_GmLink_(link, finger_GmLinkScheme); 225 setScheme_GmLink_(link, finger_GmLinkScheme);
225 } 226 }
226 else if (equalCase_Rangecc(parts.scheme, "file")) { 227 else if (equalCase_Rangecc(parts.scheme, "file")) {
227 setScheme_GmLink_(link, file_GmLinkScheme); 228 setScheme_GmLink_(link, file_GmLinkScheme);
228 } 229 }
229 else if (equalCase_Rangecc(parts.scheme, "data")) { 230 else if (equalCase_Rangecc(parts.scheme, "data")) {
230 setScheme_GmLink_(link, data_GmLinkScheme); 231 setScheme_GmLink_(link, data_GmLinkScheme);
@@ -251,6 +252,9 @@ static iRangecc addLink_GmDocument_(iGmDocument *d, iRangecc line, iGmLinkId *li
251 endsWithCase_String(path, ".mid") || endsWithCase_String(path, ".ogg")) { 252 endsWithCase_String(path, ".mid") || endsWithCase_String(path, ".ogg")) {
252 link->flags |= audioFileExtension_GmLinkFlag; 253 link->flags |= audioFileExtension_GmLinkFlag;
253 } 254 }
255 else if (endsWithCase_String(path, ".fontpack")) {
256 link->flags |= fontpackFileExtension_GmLinkFlag;
257 }
254 delete_String(path); 258 delete_String(path);
255 } 259 }
256 /* Check if visited. */ 260 /* Check if visited. */
@@ -503,7 +507,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
503 static const char *arrow = rightArrowhead_Icon; 507 static const char *arrow = rightArrowhead_Icon;
504 static const char *envelope = envelope_Icon; 508 static const char *envelope = envelope_Icon;
505 static const char *bullet = "\u2022"; 509 static const char *bullet = "\u2022";
506 static const char *folder = "\U0001f4c1"; 510 static const char *folder = file_Icon;
507 static const char *globe = globe_Icon; 511 static const char *globe = globe_Icon;
508 static const char *quote = "\u201c"; 512 static const char *quote = "\u201c";
509 static const char *magnifyingGlass = "\U0001f50d"; 513 static const char *magnifyingGlass = "\U0001f50d";
@@ -900,77 +904,77 @@ static void doLayout_GmDocument_(iGmDocument *d) {
900 ((iGmRun *) back_Array(&d->layout))->flags |= endOfLine_GmRunFlag; 904 ((iGmRun *) back_Array(&d->layout))->flags |= endOfLine_GmRunFlag;
901 /* Image or audio content. */ 905 /* Image or audio content. */
902 if (type == link_GmLineType) { 906 if (type == link_GmLineType) {
903 const iMediaId imageId = findLinkImage_Media(d->media, run.linkId); 907 /* TODO: Cleanup here? Move to a function of its own. */
904 const iMediaId audioId = !imageId ? findLinkAudio_Media(d->media, run.linkId) : 0; 908// enum iMediaType mediaType = none_MediaType;
905 const iMediaId downloadId = !imageId && !audioId ? findLinkDownload_Media(d->media, run.linkId) : 0; 909 const iMediaId media = findMediaForLink_Media(d->media, run.linkId, none_MediaType);
906 if (imageId) { 910 iGmMediaInfo info;
907 iGmMediaInfo img; 911 info_Media(d->media, media, &info);
908 imageInfo_Media(d->media, imageId, &img); 912 run.mediaType = media.type;
909 const iInt2 imgSize = imageSize_Media(d->media, imageId); 913 run.mediaId = media.id;
910 linkContentWasLaidOut_GmDocument_(d, &img, run.linkId); 914 run.text = iNullRange;
911 const int margin = lineHeight_Text(paragraph_FontId) / 2; 915 run.font = uiLabel_FontId;
916 run.color = 0;
917 const int margin = lineHeight_Text(paragraph_FontId) / 2;
918 if (media.type) {
912 pos.y += margin; 919 pos.y += margin;
913 run.bounds.pos = pos; 920 run.bounds.size.y = 0;
914 run.bounds.size.x = d->size.x;
915 const float aspect = (float) imgSize.y / (float) imgSize.x;
916 run.bounds.size.y = d->size.x * aspect;
917 /* Extend the image to full width, including outside margin, if the viewport
918 is narrow enough. */
919 if (isFullWidthImages) {
920 run.bounds.size.x += d->outsideMargin * 2;
921 run.bounds.size.y += d->outsideMargin * 2 * aspect;
922 run.bounds.pos.x -= d->outsideMargin;
923 }
924 run.visBounds = run.bounds;
925 const iInt2 maxSize = mulf_I2(imgSize, get_Window()->pixelRatio);
926 if (width_Rect(run.visBounds) > maxSize.x) {
927 /* Don't scale the image up. */
928 run.visBounds.size.y =
929 run.visBounds.size.y * maxSize.x / width_Rect(run.visBounds);
930 run.visBounds.size.x = maxSize.x;
931 run.visBounds.pos.x = run.bounds.size.x / 2 - width_Rect(run.visBounds) / 2;
932 run.bounds.size.y = run.visBounds.size.y;
933 }
934 run.text = iNullRange;
935 run.font = 0;
936 run.color = 0;
937 run.mediaType = image_GmRunMediaType;
938 run.mediaId = imageId;
939 pushBack_Array(&d->layout, &run);
940 pos.y += run.bounds.size.y + margin;
941 }
942 else if (audioId) {
943 iGmMediaInfo info;
944 audioInfo_Media(d->media, audioId, &info);
945 linkContentWasLaidOut_GmDocument_(d, &info, run.linkId); 921 linkContentWasLaidOut_GmDocument_(d, &info, run.linkId);
946 const int margin = lineHeight_Text(paragraph_FontId) / 2;
947 pos.y += margin;
948 run.bounds.pos = pos;
949 run.bounds.size.x = d->size.x;
950 run.bounds.size.y = lineHeight_Text(uiContent_FontId) + 3 * gap_UI;
951 run.visBounds = run.bounds;
952 run.text = iNullRange;
953 run.color = 0;
954 run.mediaType = audio_GmRunMediaType;
955 run.mediaId = audioId;
956 pushBack_Array(&d->layout, &run);
957 pos.y += run.bounds.size.y + margin;
958 } 922 }
959 else if (downloadId) { 923 switch (media.type) {
960 iGmMediaInfo info; 924 case image_MediaType: {
961 downloadInfo_Media(d->media, downloadId, &info); 925 const iInt2 imgSize = imageSize_Media(d->media, media);
962 linkContentWasLaidOut_GmDocument_(d, &info, run.linkId); 926 run.bounds.pos = pos;
963 const int margin = lineHeight_Text(paragraph_FontId) / 2; 927 run.bounds.size.x = d->size.x;
964 pos.y += margin; 928 const float aspect = (float) imgSize.y / (float) imgSize.x;
965 run.bounds.pos = pos; 929 run.bounds.size.y = d->size.x * aspect;
966 run.bounds.size.x = d->size.x; 930 /* Extend the image to full width, including outside margin, if the viewport
967 run.bounds.size.y = 2 * lineHeight_Text(uiContent_FontId) + 4 * gap_UI; 931 is narrow enough. */
968 run.visBounds = run.bounds; 932 if (isFullWidthImages) {
969 run.text = iNullRange; 933 run.bounds.size.x += d->outsideMargin * 2;
970 run.color = 0; 934 run.bounds.size.y += d->outsideMargin * 2 * aspect;
971 run.mediaType = download_GmRunMediaType; 935 run.bounds.pos.x -= d->outsideMargin;
972 run.mediaId = downloadId; 936 }
973 pushBack_Array(&d->layout, &run); 937 run.visBounds = run.bounds;
938 const iInt2 maxSize = mulf_I2(imgSize, get_Window()->pixelRatio);
939 if (width_Rect(run.visBounds) > maxSize.x) {
940 /* Don't scale the image up. */
941 run.visBounds.size.y =
942 run.visBounds.size.y * maxSize.x / width_Rect(run.visBounds);
943 run.visBounds.size.x = maxSize.x;
944 run.visBounds.pos.x = run.bounds.size.x / 2 - width_Rect(run.visBounds) / 2;
945 run.bounds.size.y = run.visBounds.size.y;
946 }
947 pushBack_Array(&d->layout, &run);
948 break;
949 }
950 case audio_MediaType: {
951 run.bounds.pos = pos;
952 run.bounds.size.x = d->size.x;
953 run.bounds.size.y = lineHeight_Text(uiContent_FontId) + 3 * gap_UI;
954 run.visBounds = run.bounds;
955 pushBack_Array(&d->layout, &run);
956 break;
957 }
958 case download_MediaType: {
959 run.bounds.pos = pos;
960 run.bounds.size.x = d->size.x;
961 run.bounds.size.y = 2 * lineHeight_Text(uiContent_FontId) + 4 * gap_UI;
962 run.visBounds = run.bounds;
963 pushBack_Array(&d->layout, &run);
964 break;
965 }
966 case fontpack_MediaType: {
967 run.bounds.pos = pos;
968 run.bounds.size.x = d->size.x;
969 run.bounds.size.y = height_FontpackUI(d->media, media.id, d->size.x);
970 run.visBounds = run.bounds;
971 pushBack_Array(&d->layout, &run);
972 break;
973 }
974 default:
975 break;
976 }
977 if (media.type && run.bounds.size.y) {
974 pos.y += run.bounds.size.y + margin; 978 pos.y += run.bounds.size.y + margin;
975 } 979 }
976 } 980 }
@@ -1057,21 +1061,6 @@ const iString *url_GmDocument(const iGmDocument *d) {
1057 return &d->url; 1061 return &d->url;
1058} 1062}
1059 1063
1060#if 0
1061void reset_GmDocument(iGmDocument *d) {
1062 clear_Media(d->media);
1063 clearLinks_GmDocument_(d);
1064 clear_Array(&d->layout);
1065 clear_Array(&d->headings);
1066 clear_Array(&d->preMeta);
1067 clear_String(&d->url);
1068 clear_String(&d->localHost);
1069 clear_String(&d->source);
1070 clear_String(&d->unormSource);
1071 d->themeSeed = 0;
1072}
1073#endif
1074
1075static void setDerivedThemeColors_(enum iGmDocumentTheme theme) { 1064static void setDerivedThemeColors_(enum iGmDocumentTheme theme) {
1076 set_Color(tmQuoteIcon_ColorId, 1065 set_Color(tmQuoteIcon_ColorId,
1077 mix_Color(get_Color(tmQuote_ColorId), get_Color(tmBackground_ColorId), 0.55f)); 1066 mix_Color(get_Color(tmQuote_ColorId), get_Color(tmBackground_ColorId), 0.55f));
diff --git a/src/gmdocument.h b/src/gmdocument.h
index b2c6d9b7..20bc9890 100644
--- a/src/gmdocument.h
+++ b/src/gmdocument.h
@@ -90,6 +90,7 @@ enum iGmLinkFlag {
90 query_GmLinkFlag = iBit(14), /* Gopher query link */ 90 query_GmLinkFlag = iBit(14), /* Gopher query link */
91 iconFromLabel_GmLinkFlag = iBit(15), /* use an Emoji/special character from label */ 91 iconFromLabel_GmLinkFlag = iBit(15), /* use an Emoji/special character from label */
92 isOpen_GmLinkFlag = iBit(16), /* currently open in a tab */ 92 isOpen_GmLinkFlag = iBit(16), /* currently open in a tab */
93 fontpackFileExtension_GmLinkFlag = iBit(17),
93}; 94};
94 95
95iLocalDef enum iGmLinkScheme scheme_GmLinkFlag(int flags) { 96iLocalDef enum iGmLinkScheme scheme_GmLinkFlag(int flags) {
@@ -126,13 +127,6 @@ enum iGmRunFlags {
126 altText_GmRunFlag = iBit(8), 127 altText_GmRunFlag = iBit(8),
127}; 128};
128 129
129enum iGmRunMediaType {
130 none_GmRunMediaType,
131 image_GmRunMediaType,
132 audio_GmRunMediaType,
133 download_GmRunMediaType,
134};
135
136/* This structure is tightly packed because GmDocuments are mostly composed of 130/* This structure is tightly packed because GmDocuments are mostly composed of
137 a large number of GmRuns. */ 131 a large number of GmRuns. */
138struct Impl_GmRun { 132struct Impl_GmRun {
@@ -146,12 +140,16 @@ struct Impl_GmRun {
146 uint32_t color : 7; /* see max_ColorId */ 140 uint32_t color : 7; /* see max_ColorId */
147 141
148 uint32_t font : 10; 142 uint32_t font : 10;
149 uint32_t mediaType : 2; 143 uint32_t mediaType : 3;
150 uint32_t mediaId : 10; /* zero if not an image */ 144 uint32_t mediaId : 9; /* zero if not an image */
151 uint32_t preId : 10; /* preformatted block ID (sequential); merge with mediaId? */ 145 uint32_t preId : 10; /* preformatted block ID (sequential); merge with mediaId? */
152 }; 146 };
153}; 147};
154 148
149iLocalDef iMediaId mediaId_GmRun(const iGmRun *d) {
150 return (iMediaId){ .type = d->mediaType, .id = d->mediaId };
151}
152
155iDeclareType(GmRunRange) 153iDeclareType(GmRunRange)
156 154
157struct Impl_GmRunRange { 155struct Impl_GmRunRange {
diff --git a/src/gmrequest.c b/src/gmrequest.c
index 1a9e83a9..f7a22e0a 100644
--- a/src/gmrequest.c
+++ b/src/gmrequest.c
@@ -361,6 +361,9 @@ static const iBlock *aboutPageSource_(iRangecc path, iRangecc query) {
361 if (equalCase_Rangecc(path, "debug")) { 361 if (equalCase_Rangecc(path, "debug")) {
362 return utf8_String(debugInfo_App()); 362 return utf8_String(debugInfo_App());
363 } 363 }
364 if (equalCase_Rangecc(path, "fonts")) {
365 return utf8_String(infoPage_Fonts());
366 }
364 if (equalCase_Rangecc(path, "feeds")) { 367 if (equalCase_Rangecc(path, "feeds")) {
365 return utf8_String(entryListPage_Feeds()); 368 return utf8_String(entryListPage_Feeds());
366 } 369 }
@@ -710,8 +713,9 @@ void submit_GmRequest(iGmRequest *d) {
710 sort_Array(sortedInfo, (int (*)(const void *, const void *)) cmp_FileInfoPtr_); 713 sort_Array(sortedInfo, (int (*)(const void *, const void *)) cmp_FileInfoPtr_);
711 iForEach(PtrArray, s, sortedInfo) { 714 iForEach(PtrArray, s, sortedInfo) {
712 const iFileInfo *entry = s.ptr; 715 const iFileInfo *entry = s.ptr;
713 appendFormat_String(page, "=> %s %s%s\n", 716 appendFormat_String(page, "=> %s %s%s%s\n",
714 cstrCollect_String(makeFileUrl_String(path_FileInfo(entry))), 717 cstrCollect_String(makeFileUrl_String(path_FileInfo(entry))),
718 isDirectory_FileInfo(entry) ? folder_Icon " " : "",
715 cstr_Rangecc(baseName_Path(path_FileInfo(entry))), 719 cstr_Rangecc(baseName_Path(path_FileInfo(entry))),
716 isDirectory_FileInfo(entry) ? iPathSeparator : ""); 720 isDirectory_FileInfo(entry) ? iPathSeparator : "");
717 iRelease(entry); 721 iRelease(entry);
@@ -808,9 +812,10 @@ void submit_GmRequest(iGmRequest *d) {
808 const iString *subPath = e.value; 812 const iString *subPath = e.value;
809 iRangecc relSub = range_String(subPath); 813 iRangecc relSub = range_String(subPath);
810 relSub.start += size_String(entryPath); 814 relSub.start += size_String(entryPath);
811 appendFormat_String(page, "=> %s/%s %s\n", 815 appendFormat_String(page, "=> %s/%s %s%s\n",
812 cstr_String(&d->url), 816 cstr_String(&d->url),
813 cstr_String(withSpacesEncoded_String(collectNewRange_String(relSub))), 817 cstr_String(withSpacesEncoded_String(collectNewRange_String(relSub))),
818 endsWith_Rangecc(relSub, "/") ? folder_Icon " " : "",
814 cstr_Rangecc(relSub)); 819 cstr_Rangecc(relSub));
815 } 820 }
816 resp->statusCode = success_GmStatusCode; 821 resp->statusCode = success_GmStatusCode;
diff --git a/src/gmutil.c b/src/gmutil.c
index 971747d4..5be7e198 100644
--- a/src/gmutil.c
+++ b/src/gmutil.c
@@ -21,6 +21,7 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ 21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22 22
23#include "gmutil.h" 23#include "gmutil.h"
24#include "fontpack.h"
24 25
25#include <the_Foundation/file.h> 26#include <the_Foundation/file.h>
26#include <the_Foundation/fileinfo.h> 27#include <the_Foundation/fileinfo.h>
@@ -511,7 +512,8 @@ const iString *findContainerArchive_Path(const iString *path) {
511 while (!isEmpty_String(path) && cmp_String(path, ".")) { 512 while (!isEmpty_String(path) && cmp_String(path, ".")) {
512 iString *dir = newRange_String(dirName_Path(path)); 513 iString *dir = newRange_String(dirName_Path(path));
513 if (endsWithCase_String(dir, ".zip") || 514 if (endsWithCase_String(dir, ".zip") ||
514 endsWithCase_String(dir, ".gpub")) { 515 endsWithCase_String(dir, ".gpub") ||
516 endsWithCase_String(dir, ".fontpack")) {
515 iEndCollect(); 517 iEndCollect();
516 return collect_String(dir); 518 return collect_String(dir);
517 } 519 }
@@ -534,6 +536,9 @@ const char *mediaTypeFromFileExtension_String(const iString *d) {
534 else if (endsWithCase_String(d, ".gpub")) { 536 else if (endsWithCase_String(d, ".gpub")) {
535 return "application/gpub+zip"; 537 return "application/gpub+zip";
536 } 538 }
539 else if (endsWithCase_String(d, ".fontpack")) {
540 return mimeType_FontPack;
541 }
537 else if (endsWithCase_String(d, ".xml")) { 542 else if (endsWithCase_String(d, ".xml")) {
538 return "text/xml"; 543 return "text/xml";
539 } 544 }
@@ -562,6 +567,7 @@ const char *mediaTypeFromFileExtension_String(const iString *d) {
562 return "audio/midi"; 567 return "audio/midi";
563 } 568 }
564 else if (endsWithCase_String(d, ".txt") || 569 else if (endsWithCase_String(d, ".txt") ||
570 endsWithCase_String(d, ".ini") ||
565 endsWithCase_String(d, ".md") || 571 endsWithCase_String(d, ".md") ||
566 endsWithCase_String(d, ".c") || 572 endsWithCase_String(d, ".c") ||
567 endsWithCase_String(d, ".h") || 573 endsWithCase_String(d, ".h") ||
diff --git a/src/media.c b/src/media.c
index 26f0af4b..0ce2ac5c 100644
--- a/src/media.c
+++ b/src/media.c
@@ -36,6 +36,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
36 36
37#include <the_Foundation/file.h> 37#include <the_Foundation/file.h>
38#include <the_Foundation/ptrarray.h> 38#include <the_Foundation/ptrarray.h>
39#include <the_Foundation/stringlist.h>
39#include <SDL_hints.h> 40#include <SDL_hints.h>
40#include <SDL_render.h> 41#include <SDL_render.h>
41#include <SDL_timer.h> 42#include <SDL_timer.h>
@@ -287,45 +288,112 @@ iDefineTypeConstruction(GmDownload)
287 288
288/*----------------------------------------------------------------------------------------------*/ 289/*----------------------------------------------------------------------------------------------*/
289 290
291iDeclareType(GmFontpack)
292
293struct Impl_GmFontpack {
294 iGmMediaProps props;
295 iString packId;
296 iFontpackMediaInfo info;
297 /* TODO: Font preview images? */
298};
299
300void init_GmFontpack(iGmFontpack *d) {
301 init_GmMediaProps_(&d->props);
302 init_String(&d->packId);
303 iZap(d->info);
304 d->info.names = new_StringList();
305}
306
307void deinit_GmFontpack(iGmFontpack *d) {
308 iRelease(d->info.names);
309 deinit_String(&d->packId);
310 deinit_GmMediaProps_(&d->props);
311}
312
313static void loadData_GmFontpack_(iGmFontpack *d, const iBlock *data) {
314 const iString *loadPath = collect_String(localFilePathFromUrl_String(&d->props.url));
315 const iFontPack *pack = findPack_Fonts(loadPath);
316 d->info.isValid = d->info.isInstalled = pack != NULL;
317 d->info.isReadOnly = iFalse;
318 if (!pack) {
319 /* Let's load it now temporarily and see what's inside. */
320 iArchive *zip = new_Archive();
321 if (openData_Archive(zip, data)) {
322 iFontPack *fp = collect_FontPack(new_FontPack());
323 setLoadPath_FontPack(fp, loadPath);
324 setStandalone_FontPack(fp, iTrue);
325 if (loadArchive_FontPack(fp, zip)) {
326 d->info.isValid = iTrue;
327 pack = fp;
328 }
329 }
330 iRelease(zip);
331 }
332 if (pack) {
333 set_String(&d->packId, id_FontPack(pack).id);
334 d->info.packId.id = &d->packId; /* we own this String */
335 d->info.packId.version = id_FontPack(pack).version;
336 d->info.isReadOnly = isReadOnly_FontPack(pack);
337 }
338 iPtrSet *unique = new_PtrSet();
339 iConstForEach(PtrArray, i, listSpecs_FontPack(pack)) {
340 const iFontSpec *spec = i.ptr;
341 pushBack_StringList(d->info.names, &spec->name);
342 iForIndices(j, spec->styles) {
343 insert_PtrSet(unique, spec->styles[j]);
344 }
345 }
346 iConstForEach(PtrSet, j, unique) {
347 d->info.sizeInBytes += size_Block(&((const iFontFile *) *j.value)->sourceData);
348 }
349 delete_PtrSet(unique);
350}
351
352iDefineTypeConstruction(GmFontpack)
353
354/*----------------------------------------------------------------------------------------------*/
355
290struct Impl_Media { 356struct Impl_Media {
291 iPtrArray images; 357 iPtrArray items[max_MediaType];
292 iPtrArray audio; 358 /* TODO: Add a hash to quickly look up a link's media. */
293 iPtrArray downloads;
294}; 359};
295 360
296iDefineTypeConstruction(Media) 361iDefineTypeConstruction(Media)
297 362
298void init_Media(iMedia *d) { 363void init_Media(iMedia *d) {
299 init_PtrArray(&d->images); 364 iForIndices(i, d->items) {
300 init_PtrArray(&d->audio); 365 init_PtrArray(&d->items[i]);
301 init_PtrArray(&d->downloads); 366 }
302} 367}
303 368
304void deinit_Media(iMedia *d) { 369void deinit_Media(iMedia *d) {
305 clear_Media(d); 370 clear_Media(d);
306 deinit_PtrArray(&d->downloads); 371 iForIndices(i, d->items) {
307 deinit_PtrArray(&d->audio); 372 deinit_PtrArray(&d->items[i]);
308 deinit_PtrArray(&d->images); 373 }
309} 374}
310 375
311void clear_Media(iMedia *d) { 376void clear_Media(iMedia *d) {
312 iForEach(PtrArray, i, &d->images) { 377 iForEach(PtrArray, i, &d->items[image_MediaType]) {
313 deinit_GmImage(i.ptr); 378 deinit_GmImage(i.ptr);
314 } 379 }
315 clear_PtrArray(&d->images); 380 iForEach(PtrArray, a, &d->items[audio_MediaType]) {
316 iForEach(PtrArray, a, &d->audio) {
317 deinit_GmAudio(a.ptr); 381 deinit_GmAudio(a.ptr);
318 } 382 }
319 clear_PtrArray(&d->audio); 383 iForEach(PtrArray, n, &d->items[download_MediaType]) {
320 iForEach(PtrArray, n, &d->downloads) {
321 deinit_GmDownload(n.ptr); 384 deinit_GmDownload(n.ptr);
322 } 385 }
323 clear_PtrArray(&d->downloads); 386 iForEach(PtrArray, f, &d->items[fontpack_MediaType]) {
387 deinit_GmFontpack(f.ptr);
388 }
389 iForIndices(type, d->items) {
390 clear_PtrArray(&d->items[type]);
391 }
324} 392}
325 393
326size_t memorySize_Media(const iMedia *d) { 394size_t memorySize_Media(const iMedia *d) {
327 size_t memSize = 0; 395 size_t memSize = 0;
328 iConstForEach(PtrArray, i, &d->images) { 396 iConstForEach(PtrArray, i, &d->items[image_MediaType]) {
329 const iGmImage *img = i.ptr; 397 const iGmImage *img = i.ptr;
330 if (img->texture) { 398 if (img->texture) {
331 const iInt2 texSize = size_SDLTexture(img->texture); 399 const iInt2 texSize = size_SDLTexture(img->texture);
@@ -335,34 +403,49 @@ size_t memorySize_Media(const iMedia *d) {
335 memSize += size_Block(&img->partialData); 403 memSize += size_Block(&img->partialData);
336 } 404 }
337 } 405 }
338 iConstForEach(PtrArray, a, &d->audio) { 406 iConstForEach(PtrArray, a, &d->items[audio_MediaType]) {
339 const iGmAudio *audio = a.ptr; 407 const iGmAudio *audio = a.ptr;
340 if (audio->player) { 408 if (audio->player) {
341 memSize += sourceDataSize_Player(audio->player); 409 memSize += sourceDataSize_Player(audio->player);
342 } 410 }
343 } 411 }
344 iConstForEach(PtrArray, n, &d->downloads) { 412 iConstForEach(PtrArray, n, &d->items[download_MediaType]) {
345 const iGmDownload *down = n.ptr; 413 const iGmDownload *down = n.ptr;
346 memSize += down->numBytes; 414 memSize += down->numBytes;
347 } 415 }
348 return memSize; 416 return memSize;
349} 417}
350 418
351iBool setDownloadUrl_Media(iMedia *d, iGmLinkId linkId, const iString *url) { 419iBool setUrl_Media(iMedia *d, iGmLinkId linkId, enum iMediaType mediaType, const iString *url) {
352 iGmDownload *dl = NULL; 420 iMediaId existing = findMediaForLink_Media(d, linkId, mediaType);
353 iMediaId existing = findLinkDownload_Media(d, linkId); 421 const iBool isNew = !existing.id;
354 iBool isNew = iFalse; 422 iGmMediaProps *props = NULL;
355 if (!existing) { 423 if (mediaType == download_MediaType) {
356 isNew = iTrue; 424 iGmDownload *dl = NULL;
357 dl = new_GmDownload(); 425 if (isNew) {
358 dl->props.linkId = linkId; 426 dl = new_GmDownload();
359 dl->props.isPermanent = iTrue; 427 pushBack_PtrArray(&d->items[download_MediaType], dl);
360 set_String(&dl->props.url, url); 428 }
361 pushBack_PtrArray(&d->downloads, dl); 429 else {
430 dl = at_PtrArray(&d->items[download_MediaType], index_MediaId(existing));
431 }
432 props = &dl->props;
362 } 433 }
363 else { 434 else if (mediaType == fontpack_MediaType) {
364 iGmDownload *dl = at_PtrArray(&d->downloads, existing - 1); 435 iGmFontpack *fp = NULL;
365 set_String(&dl->props.url, url); 436 if (isNew) {
437 fp = new_GmFontpack();
438 pushBack_PtrArray(&d->items[fontpack_MediaType], fp);
439 }
440 else {
441 fp = at_PtrArray(&d->items[fontpack_MediaType], index_MediaId(existing));
442 }
443 props = &fp->props;
444 }
445 if (props) {
446 props->linkId = linkId;
447 props->isPermanent = iTrue;
448 set_String(&props->url, url);
366 } 449 }
367 return isNew; 450 return isNew;
368} 451}
@@ -372,16 +455,17 @@ iBool setData_Media(iMedia *d, iGmLinkId linkId, const iString *mime, const iBlo
372 const iBool isPartial = (flags & partialData_MediaFlag) != 0; 455 const iBool isPartial = (flags & partialData_MediaFlag) != 0;
373 const iBool allowHide = (flags & allowHide_MediaFlag) != 0; 456 const iBool allowHide = (flags & allowHide_MediaFlag) != 0;
374 const iBool isDeleting = (!mime || !data); 457 const iBool isDeleting = (!mime || !data);
375 iMediaId existing = findLinkImage_Media(d, linkId); 458 iMediaId existing = findMediaForLink_Media(d, linkId, none_MediaType);// findLinkImage_Media(d, linkId);
459 const size_t existingIndex = index_MediaId(existing);
376 iBool isNew = iFalse; 460 iBool isNew = iFalse;
377 if (existing) { 461 if (existing.type == image_MediaType) {
378 iGmImage *img; 462 iGmImage *img;
379 if (isDeleting) { 463 if (isDeleting) {
380 take_PtrArray(&d->images, existing - 1, (void **) &img); 464 take_PtrArray(&d->items[image_MediaType], existingIndex, (void **) &img);
381 delete_GmImage(img); 465 delete_GmImage(img);
382 } 466 }
383 else { 467 else {
384 img = at_PtrArray(&d->images, existing - 1); 468 img = at_PtrArray(&d->items[image_MediaType], existingIndex);
385 iAssert(equal_String(&img->props.mime, mime)); /* MIME cannot change */ 469 iAssert(equal_String(&img->props.mime, mime)); /* MIME cannot change */
386 set_Block(&img->partialData, data); 470 set_Block(&img->partialData, data);
387 if (!isPartial) { 471 if (!isPartial) {
@@ -389,14 +473,14 @@ iBool setData_Media(iMedia *d, iGmLinkId linkId, const iString *mime, const iBlo
389 } 473 }
390 } 474 }
391 } 475 }
392 else if ((existing = findLinkAudio_Media(d, linkId)) != 0) { 476 else if (existing.type == audio_MediaType) {
393 iGmAudio *audio; 477 iGmAudio *audio;
394 if (isDeleting) { 478 if (isDeleting) {
395 take_PtrArray(&d->audio, existing - 1, (void **) &audio); 479 take_PtrArray(&d->items[audio_MediaType], existingIndex, (void **) &audio);
396 delete_GmAudio(audio); 480 delete_GmAudio(audio);
397 } 481 }
398 else { 482 else {
399 audio = at_PtrArray(&d->audio, existing - 1); 483 audio = at_PtrArray(&d->items[audio_MediaType], existingIndex);
400 iAssert(equal_String(&audio->props.mime, mime)); /* MIME cannot change */ 484 iAssert(equal_String(&audio->props.mime, mime)); /* MIME cannot change */
401 updateSourceData_Player(audio->player, mime, data, append_PlayerUpdate); 485 updateSourceData_Player(audio->player, mime, data, append_PlayerUpdate);
402 if (!isPartial) { 486 if (!isPartial) {
@@ -408,14 +492,14 @@ iBool setData_Media(iMedia *d, iGmLinkId linkId, const iString *mime, const iBlo
408 } 492 }
409 } 493 }
410 } 494 }
411 else if ((existing = findLinkDownload_Media(d, linkId)) != 0) { 495 else if (existing.type == download_MediaType) {
412 iGmDownload *dl; 496 iGmDownload *dl;
413 if (isDeleting) { 497 if (isDeleting) {
414 take_PtrArray(&d->downloads, existing - 1, (void **) &dl); 498 take_PtrArray(&d->items[download_MediaType], existingIndex, (void **) &dl);
415 delete_GmDownload(dl); 499 delete_GmDownload(dl);
416 } 500 }
417 else { 501 else {
418 dl = at_PtrArray(&d->downloads, existing - 1); 502 dl = at_PtrArray(&d->items[download_MediaType], existingIndex);
419 if (isEmpty_String(&dl->props.mime)) { 503 if (isEmpty_String(&dl->props.mime)) {
420 set_String(&dl->props.mime, mime); 504 set_String(&dl->props.mime, mime);
421 } 505 }
@@ -428,6 +512,21 @@ iBool setData_Media(iMedia *d, iGmLinkId linkId, const iString *mime, const iBlo
428 } 512 }
429 } 513 }
430 } 514 }
515 else if (existing.type == fontpack_MediaType) {
516 iGmFontpack *fp;
517 if (isDeleting) {
518 take_PtrArray(&d->items[fontpack_MediaType], existingIndex, (void **) &fp);
519 delete_GmFontpack(fp);
520 }
521 else {
522 iAssert(!isPartial);
523 fp = at_PtrArray(&d->items[fontpack_MediaType], existingIndex);
524 if (isEmpty_String(&fp->props.mime)) {
525 set_String(&fp->props.mime, mime);
526 }
527 loadData_GmFontpack_(fp, data);
528 }
529 }
431 else if (!isDeleting) { 530 else if (!isDeleting) {
432 if (startsWith_String(mime, "image/")) { 531 if (startsWith_String(mime, "image/")) {
433 /* Copy the image to a texture. */ 532 /* Copy the image to a texture. */
@@ -435,7 +534,7 @@ iBool setData_Media(iMedia *d, iGmLinkId linkId, const iString *mime, const iBlo
435 img->props.linkId = linkId; /* TODO: use a hash? */ 534 img->props.linkId = linkId; /* TODO: use a hash? */
436 img->props.isPermanent = !allowHide; 535 img->props.isPermanent = !allowHide;
437 set_String(&img->props.mime, mime); 536 set_String(&img->props.mime, mime);
438 pushBack_PtrArray(&d->images, img); 537 pushBack_PtrArray(&d->items[image_MediaType], img);
439 if (!isPartial) { 538 if (!isPartial) {
440 makeTexture_GmImage(img); 539 makeTexture_GmImage(img);
441 } 540 }
@@ -450,7 +549,7 @@ iBool setData_Media(iMedia *d, iGmLinkId linkId, const iString *mime, const iBlo
450 if (!isPartial) { 549 if (!isPartial) {
451 updateSourceData_Player(audio->player, NULL, NULL, complete_PlayerUpdate); 550 updateSourceData_Player(audio->player, NULL, NULL, complete_PlayerUpdate);
452 } 551 }
453 pushBack_PtrArray(&d->audio, audio); 552 pushBack_PtrArray(&d->items[audio_MediaType], audio);
454 /* Start playing right away. */ 553 /* Start playing right away. */
455 start_Player(audio->player); 554 start_Player(audio->player);
456 postCommandf_App("media.player.started player:%p", audio->player); 555 postCommandf_App("media.player.started player:%p", audio->player);
@@ -460,125 +559,135 @@ iBool setData_Media(iMedia *d, iGmLinkId linkId, const iString *mime, const iBlo
460 return isNew; 559 return isNew;
461} 560}
462 561
463iMediaId findLinkImage_Media(const iMedia *d, iGmLinkId linkId) { 562static iMediaId findMediaPtr_Media_(const iPtrArray *items, enum iMediaType mediaType, iGmLinkId linkId) {
464 /* TODO: use a hash */ 563 iConstForEach(PtrArray, i, items) {
465 iConstForEach(PtrArray, i, &d->images) { 564 const iGmMediaProps *props = i.ptr;
466 const iGmImage *img = i.ptr; 565 if (props->linkId == linkId) {
467 if (img->props.linkId == linkId) { 566 return (iMediaId){
468 return index_PtrArrayConstIterator(&i) + 1; 567 .type = mediaType,
568 .id = index_PtrArrayConstIterator(&i) + 1
569 };
469 } 570 }
470 } 571 }
471 return 0; 572 return iInvalidMediaId;
472}
473
474size_t numAudio_Media(const iMedia *d) {
475 return size_PtrArray(&d->audio);
476} 573}
477 574
478iMediaId findLinkAudio_Media(const iMedia *d, iGmLinkId linkId) { 575iMediaId findMediaForLink_Media(const iMedia *d, iGmLinkId linkId, enum iMediaType mediaType) {
479 /* TODO: use a hash */ 576 /* TODO: Use hashes, this will get very slow if there is a large number of media items. */
480 iConstForEach(PtrArray, i, &d->audio) { 577 iMediaId mid;
481 const iGmAudio *audio = i.ptr; 578 for (int i = 0; i < max_MediaType; i++) {
482 if (audio->props.linkId == linkId) { 579 if (mediaType == i || !mediaType) {
483 return index_PtrArrayConstIterator(&i) + 1; 580 mid = findMediaPtr_Media_(&d->items[i], i, linkId);
581 if (mid.type) {
582 return mid;
583 }
484 } 584 }
485 } 585 }
486 return 0; 586 return iInvalidMediaId;
487} 587}
488 588
489iMediaId findLinkDownload_Media(const iMedia *d, uint16_t linkId) { 589size_t numAudio_Media(const iMedia *d) {
490 iConstForEach(PtrArray, i, &d->downloads) { 590 return size_PtrArray(&d->items[audio_MediaType]);
491 const iGmDownload *dl = i.ptr;
492 if (dl->props.linkId == linkId) {
493 return index_PtrArrayConstIterator(&i) + 1;
494 }
495 }
496 return 0;
497} 591}
498 592
499iInt2 imageSize_Media(const iMedia *d, iMediaId imageId) { 593iInt2 imageSize_Media(const iMedia *d, iMediaId imageId) {
500 if (imageId > 0 && imageId <= size_PtrArray(&d->images)) { 594 iAssert(imageId.type == image_MediaType);
501 const iGmImage *img = constAt_PtrArray(&d->images, imageId - 1); 595 const size_t index = index_MediaId(imageId);
596 if (index < size_PtrArray(&d->items[image_MediaType])) {
597 const iGmImage *img = constAt_PtrArray(&d->items[image_MediaType], index);
502 return img->size; 598 return img->size;
503 } 599 }
504 return zero_I2(); 600 return zero_I2();
505} 601}
506 602
507SDL_Texture *imageTexture_Media(const iMedia *d, uint16_t imageId) { 603SDL_Texture *imageTexture_Media(const iMedia *d, iMediaId imageId) {
508 if (imageId > 0 && imageId <= size_PtrArray(&d->images)) { 604 iAssert(imageId.type == image_MediaType);
509 const iGmImage *img = constAt_PtrArray(&d->images, imageId - 1); 605 const size_t index = index_MediaId(imageId);
606 if (index < size_PtrArray(&d->items[image_MediaType])) {
607 const iGmImage *img = constAt_PtrArray(&d->items[image_MediaType], index);
510 return img->texture; 608 return img->texture;
511 } 609 }
512 return NULL; 610 return NULL;
513} 611}
514 612
515iBool imageInfo_Media(const iMedia *d, iMediaId imageId, iGmMediaInfo *info_out) { 613iBool info_Media(const iMedia *d, iMediaId mediaId, iGmMediaInfo *info_out) {
516 if (imageId > 0 && imageId <= size_PtrArray(&d->images)) { 614 /* TODO: Use a hash. */
517 const iGmImage *img = constAt_PtrArray(&d->images, imageId - 1); 615 const size_t index = index_MediaId(mediaId);
518 info_out->numBytes = img->numBytes; 616 switch (mediaId.type) {
519 info_out->type = cstr_String(&img->props.mime); 617 case image_MediaType:
520 info_out->isPermanent = img->props.isPermanent; 618 if (index < size_PtrArray(&d->items[image_MediaType])) {
521 return iTrue; 619 const iGmImage *img = constAt_PtrArray(&d->items[image_MediaType], index);
620 info_out->numBytes = img->numBytes;
621 info_out->type = cstr_String(&img->props.mime);
622 info_out->isPermanent = img->props.isPermanent;
623 return iTrue;
624 }
625 break;
626 case audio_MediaType:
627 if (index < size_PtrArray(&d->items[audio_MediaType])) {
628 const iGmAudio *audio = constAt_PtrArray(&d->items[audio_MediaType], index);
629 info_out->type = cstr_String(&audio->props.mime);
630 info_out->isPermanent = audio->props.isPermanent;
631 return iTrue;
632 }
633 break;
634 case download_MediaType:
635 if (index < size_PtrArray(&d->items[download_MediaType])) {
636 const iGmDownload *dl = constAt_PtrArray(&d->items[download_MediaType], index);
637 info_out->type = cstr_String(&dl->props.mime);
638 info_out->isPermanent = dl->props.isPermanent;
639 info_out->numBytes = dl->numBytes;
640 return iTrue;
641 }
642 break;
643 case fontpack_MediaType:
644 /* TODO */
645 break;
646 default:
647 break;
522 } 648 }
523 iZap(*info_out); 649 iZap(*info_out);
524 return iFalse; 650 return iFalse;
525} 651}
526 652
527iPlayer *audioData_Media(const iMedia *d, iMediaId audioId) { 653iPlayer *audioData_Media(const iMedia *d, iMediaId audioId) {
528 if (audioId > 0 && audioId <= size_PtrArray(&d->audio)) { 654 iAssert(audioId.type == audio_MediaType);
529 const iGmAudio *audio = constAt_PtrArray(&d->audio, audioId - 1); 655 const size_t index = index_MediaId(audioId);
656 if (index < size_PtrArray(&d->items[audio_MediaType])) {
657 const iGmAudio *audio = constAt_PtrArray(&d->items[audio_MediaType], index);
530 return audio->player; 658 return audio->player;
531 } 659 }
532 return NULL; 660 return NULL;
533} 661}
534 662
535iBool audioInfo_Media(const iMedia *d, iMediaId audioId, iGmMediaInfo *info_out) {
536 if (audioId > 0 && audioId <= size_PtrArray(&d->audio)) {
537 const iGmAudio *audio = constAt_PtrArray(&d->audio, audioId - 1);
538 info_out->type = cstr_String(&audio->props.mime);
539 info_out->isPermanent = audio->props.isPermanent;
540 return iTrue;
541 }
542 iZap(*info_out);
543 return iFalse;
544}
545
546iPlayer *audioPlayer_Media(const iMedia *d, iMediaId audioId) { 663iPlayer *audioPlayer_Media(const iMedia *d, iMediaId audioId) {
547 if (audioId > 0 && audioId <= size_PtrArray(&d->audio)) { 664 iAssert(audioId.type == audio_MediaType);
548 const iGmAudio *audio = constAt_PtrArray(&d->audio, audioId - 1); 665 const size_t index = index_MediaId(audioId);
666 if (index < size_PtrArray(&d->items[audio_MediaType])) {
667 const iGmAudio *audio = constAt_PtrArray(&d->items[audio_MediaType], index);
549 return audio->player; 668 return audio->player;
550 } 669 }
551 return NULL; 670 return NULL;
552} 671}
553 672
554void pauseAllPlayers_Media(const iMedia *d, iBool setPaused) { 673void pauseAllPlayers_Media(const iMedia *d, iBool setPaused) {
555 for (size_t i = 0; i < size_PtrArray(&d->audio); ++i) { 674 for (size_t i = 0; i < size_PtrArray(&d->items[audio_MediaType]); ++i) {
556 const iGmAudio *audio = constAt_PtrArray(&d->audio, i); 675 const iGmAudio *audio = constAt_PtrArray(&d->items[audio_MediaType], i);
557 if (audio->player) { 676 if (audio->player) {
558 setPaused_Player(audio->player, setPaused); 677 setPaused_Player(audio->player, setPaused);
559 } 678 }
560 } 679 }
561} 680}
562 681
563iBool downloadInfo_Media(const iMedia *d, iMediaId downloadId, iGmMediaInfo *info_out) {
564 if (downloadId > 0 && downloadId <= size_PtrArray(&d->downloads)) {
565 const iGmDownload *dl = constAt_PtrArray(&d->downloads, downloadId - 1);
566 info_out->type = cstr_String(&dl->props.mime);
567 info_out->isPermanent = dl->props.isPermanent;
568 info_out->numBytes = dl->numBytes;
569 return iTrue;
570 }
571 iZap(*info_out);
572 return iFalse;
573}
574
575void downloadStats_Media(const iMedia *d, iMediaId downloadId, const iString **path_out, 682void downloadStats_Media(const iMedia *d, iMediaId downloadId, const iString **path_out,
576 float *bytesPerSecond_out, iBool *isFinished_out) { 683 float *bytesPerSecond_out, iBool *isFinished_out) {
577 *path_out = NULL; 684 iAssert(downloadId.type == download_MediaType);
685 *path_out = NULL;
578 *bytesPerSecond_out = 0.0f; 686 *bytesPerSecond_out = 0.0f;
579 *isFinished_out = iFalse; 687 *isFinished_out = iFalse;
580 if (downloadId > 0 && downloadId <= size_PtrArray(&d->downloads)) { 688 const size_t index = index_MediaId(downloadId);
581 const iGmDownload *dl = constAt_PtrArray(&d->downloads, downloadId - 1); 689 if (index < size_PtrArray(&d->items[download_MediaType])) {
690 const iGmDownload *dl = constAt_PtrArray(&d->items[download_MediaType], index);
582 if (dl->path) { 691 if (dl->path) {
583 *path_out = dl->path; 692 *path_out = dl->path;
584 } 693 }
@@ -587,6 +696,16 @@ void downloadStats_Media(const iMedia *d, iMediaId downloadId, const iString **p
587 } 696 }
588} 697}
589 698
699void fontpackInfo_Media(const iMedia *d, iMediaId fontpackId, iFontpackMediaInfo *info_out) {
700 iAssert(fontpackId.type == fontpack_MediaType);
701 iZap(*info_out);
702 const size_t index = index_MediaId(fontpackId);
703 if (index < size_PtrArray(&d->items[fontpack_MediaType])) {
704 const iGmFontpack *fp = constAt_PtrArray(&d->items[fontpack_MediaType], index);
705 *info_out = fp->info;
706 }
707}
708
590/*----------------------------------------------------------------------------------------------*/ 709/*----------------------------------------------------------------------------------------------*/
591 710
592static void updated_MediaRequest_(iAnyObject *obj) { 711static void updated_MediaRequest_(iAnyObject *obj) {
diff --git a/src/media.h b/src/media.h
index f7ad6efd..47a4da93 100644
--- a/src/media.h
+++ b/src/media.h
@@ -22,13 +22,13 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22 22
23#pragma once 23#pragma once
24 24
25#include "fontpack.h"
26
25#include <the_Foundation/block.h> 27#include <the_Foundation/block.h>
26#include <the_Foundation/string.h> 28#include <the_Foundation/string.h>
27#include <the_Foundation/vec2.h> 29#include <the_Foundation/vec2.h>
28#include <SDL_render.h> 30#include <SDL_render.h>
29 31
30typedef uint16_t iMediaId;
31
32iDeclareType(Player) 32iDeclareType(Player)
33iDeclareType(GmMediaInfo) 33iDeclareType(GmMediaInfo)
34 34
@@ -38,6 +38,7 @@ struct Impl_GmMediaInfo {
38 iBool isPermanent; 38 iBool isPermanent;
39}; 39};
40 40
41iDeclareType(MediaId)
41iDeclareType(Media) 42iDeclareType(Media)
42iDeclareTypeConstruction(Media) 43iDeclareTypeConstruction(Media)
43 44
@@ -46,28 +47,81 @@ enum iMediaFlags {
46 partialData_MediaFlag = iBit(2), 47 partialData_MediaFlag = iBit(2),
47}; 48};
48 49
49void clear_Media (iMedia *); 50enum iMediaType { /* Note: There is a limited number of bits for these; see GmRun below. */
50iBool setDownloadUrl_Media (iMedia *, uint16_t linkId, const iString *url); 51 none_MediaType,
51iBool setData_Media (iMedia *, uint16_t linkId, const iString *mime, const iBlock *data, int flags); 52 image_MediaType,
52 53 //animatedImage_MediaType, /* TODO */
53size_t memorySize_Media (const iMedia *); 54 audio_MediaType,
55 download_MediaType,
56 fontpack_MediaType,
57 max_MediaType
58};
54 59
55iMediaId findLinkImage_Media (const iMedia *, uint16_t linkId); 60struct Impl_MediaId {
56iBool imageInfo_Media (const iMedia *, iMediaId imageId, iGmMediaInfo *info_out); 61 enum iMediaType type;
57iInt2 imageSize_Media (const iMedia *, iMediaId imageId); 62 uint16_t id; /* see GmRun for actually used number of bits */
58SDL_Texture * imageTexture_Media (const iMedia *, iMediaId imageId); 63};
59 64
60size_t numAudio_Media (const iMedia *); 65iLocalDef size_t index_MediaId(const iMediaId mediaId) {
61iMediaId findLinkAudio_Media (const iMedia *, uint16_t linkId); 66 return (size_t) mediaId.id - 1;
62iBool audioInfo_Media (const iMedia *, iMediaId audioId, iGmMediaInfo *info_out); 67}
63iPlayer * audioPlayer_Media (const iMedia *, iMediaId audioId); 68
64void pauseAllPlayers_Media(const iMedia *, iBool setPaused); 69#define iInvalidMediaId (iMediaId){ none_MediaType, 0 }
70
71void clear_Media (iMedia *);
72iBool setUrl_Media (iMedia *, uint16_t linkId, enum iMediaType mediaType, const iString *url);
73iBool setData_Media (iMedia *, uint16_t linkId, const iString *mime, const iBlock *data, int flags);
74
75size_t memorySize_Media (const iMedia *);
76iMediaId findMediaForLink_Media (const iMedia *, uint16_t linkId, enum iMediaType mediaType);
77
78iMediaId id_Media (const iMedia *, uint16_t linkId, enum iMediaType type);
79iBool info_Media (const iMedia *, iMediaId mediaId, iGmMediaInfo *info_out);
80
81iLocalDef iMediaId findLinkImage_Media(const iMedia *d, uint16_t linkId) {
82 return findMediaForLink_Media(d, linkId, image_MediaType);
83}
84iLocalDef iMediaId findLinkAudio_Media (const iMedia *d, uint16_t linkId) {
85 return findMediaForLink_Media(d, linkId, audio_MediaType);
86}
87iLocalDef iMediaId findLinkDownload_Media(const iMedia *d, uint16_t linkId) {
88 return findMediaForLink_Media(d, linkId, download_MediaType);
89}
90
91iLocalDef iBool imageInfo_Media(const iMedia *d, uint16_t mediaId, iGmMediaInfo *info_out) {
92 return info_Media(d, (iMediaId){ image_MediaType, mediaId }, info_out);
93}
94iLocalDef iBool audioInfo_Media(const iMedia *d, uint16_t mediaId, iGmMediaInfo *info_out) {
95 return info_Media(d, (iMediaId){ audio_MediaType, mediaId }, info_out);
96}
97iLocalDef iBool downloadInfo_Media(const iMedia *d, uint16_t mediaId, iGmMediaInfo *info_out) {
98 return info_Media(d, (iMediaId){ download_MediaType, mediaId }, info_out);
99}
100
101iInt2 imageSize_Media (const iMedia *, iMediaId imageId);
102SDL_Texture * imageTexture_Media (const iMedia *, iMediaId imageId);
103
104size_t numAudio_Media (const iMedia *);
105iPlayer * audioPlayer_Media (const iMedia *, iMediaId audioId);
106void pauseAllPlayers_Media (const iMedia *, iBool setPaused);
65 107
66iMediaId findLinkDownload_Media (const iMedia *, uint16_t linkId);
67iBool downloadInfo_Media (const iMedia *, iMediaId downloadId, iGmMediaInfo *info_out);
68void downloadStats_Media (const iMedia *, iMediaId downloadId, const iString **path_out, 108void downloadStats_Media (const iMedia *, iMediaId downloadId, const iString **path_out,
69 float *bytesPerSecond_out, iBool *isFinished_out); 109 float *bytesPerSecond_out, iBool *isFinished_out);
70 110
111iDeclareType(FontpackMediaInfo)
112
113struct Impl_FontpackMediaInfo {
114 iFontPackId packId;
115 iBool isValid;
116 iBool isInstalled;
117 iBool isReadOnly;
118 size_t sizeInBytes;
119 iStringList *names;
120};
121
122void fontpackInfo_Media (const iMedia *, iMediaId fontpackId,
123 iFontpackMediaInfo *info_out);
124
71/*----------------------------------------------------------------------------------------------*/ 125/*----------------------------------------------------------------------------------------------*/
72 126
73iDeclareType(GmRequest) 127iDeclareType(GmRequest)
@@ -78,7 +132,7 @@ iDeclareClass(MediaRequest)
78struct Impl_MediaRequest { 132struct Impl_MediaRequest {
79 iObject object; 133 iObject object;
80 iDocumentWidget *doc; 134 iDocumentWidget *doc;
81 unsigned int linkId; 135 unsigned int linkId;
82 iGmRequest * req; 136 iGmRequest * req;
83}; 137};
84 138
diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c
index 45a8cf2d..44db3e5b 100644
--- a/src/ui/documentwidget.c
+++ b/src/ui/documentwidget.c
@@ -400,7 +400,18 @@ void init_DocumentWidget(iDocumentWidget *d) {
400 addAction_Widget(w, navigateRoot_KeyShortcut, "navigate.root"); 400 addAction_Widget(w, navigateRoot_KeyShortcut, "navigate.root");
401} 401}
402 402
403void cancelAllRequests_DocumentWidget(iDocumentWidget *d) {
404 iForEach(ObjectList, i, d->media) {
405 iMediaRequest *mr = i.object;
406 cancel_GmRequest(mr->req);
407 }
408 if (d->request) {
409 cancel_GmRequest(d->request);
410 }
411}
412
403void deinit_DocumentWidget(iDocumentWidget *d) { 413void deinit_DocumentWidget(iDocumentWidget *d) {
414 cancelAllRequests_DocumentWidget(d);
404 pauseAllPlayers_Media(media_GmDocument(d->doc), iTrue); 415 pauseAllPlayers_Media(media_GmDocument(d->doc), iTrue);
405 removeTicker_App(animate_DocumentWidget_, d); 416 removeTicker_App(animate_DocumentWidget_, d);
406 removeTicker_App(prerender_DocumentWidget_, d); 417 removeTicker_App(prerender_DocumentWidget_, d);
@@ -564,7 +575,8 @@ static void addVisible_DocumentWidget_(void *context, const iGmRun *run) {
564 pushBack_PtrArray(&d->visibleWideRuns, run); 575 pushBack_PtrArray(&d->visibleWideRuns, run);
565 } 576 }
566 } 577 }
567 if (run->mediaType == audio_GmRunMediaType || run->mediaType == download_GmRunMediaType) { 578 /* Image runs are static so they're drawn as part of the content. */
579 if (run->mediaType && run->mediaType != image_MediaType) {
568 iAssert(run->mediaId); 580 iAssert(run->mediaId);
569 pushBack_PtrArray(&d->visibleMedia, run); 581 pushBack_PtrArray(&d->visibleMedia, run);
570 } 582 }
@@ -758,14 +770,14 @@ static uint32_t mediaUpdateInterval_DocumentWidget_(const iDocumentWidget *d) {
758 uint32_t interval = invalidInterval_; 770 uint32_t interval = invalidInterval_;
759 iConstForEach(PtrArray, i, &d->visibleMedia) { 771 iConstForEach(PtrArray, i, &d->visibleMedia) {
760 const iGmRun *run = i.ptr; 772 const iGmRun *run = i.ptr;
761 if (run->mediaType == audio_GmRunMediaType) { 773 if (run->mediaType == audio_MediaType) {
762 iPlayer *plr = audioPlayer_Media(media_GmDocument(d->doc), run->mediaId); 774 iPlayer *plr = audioPlayer_Media(media_GmDocument(d->doc), mediaId_GmRun(run));
763 if (flags_Player(plr) & adjustingVolume_PlayerFlag || 775 if (flags_Player(plr) & adjustingVolume_PlayerFlag ||
764 (isStarted_Player(plr) && !isPaused_Player(plr))) { 776 (isStarted_Player(plr) && !isPaused_Player(plr))) {
765 interval = iMin(interval, 1000 / 15); 777 interval = iMin(interval, 1000 / 15);
766 } 778 }
767 } 779 }
768 else if (run->mediaType == download_GmRunMediaType) { 780 else if (run->mediaType == download_MediaType) {
769 interval = iMin(interval, 1000); 781 interval = iMin(interval, 1000);
770 } 782 }
771 } 783 }
@@ -784,8 +796,8 @@ static void updateMedia_DocumentWidget_(iDocumentWidget *d) {
784 refresh_Widget(d); 796 refresh_Widget(d);
785 iConstForEach(PtrArray, i, &d->visibleMedia) { 797 iConstForEach(PtrArray, i, &d->visibleMedia) {
786 const iGmRun *run = i.ptr; 798 const iGmRun *run = i.ptr;
787 if (run->mediaType == audio_GmRunMediaType) { 799 if (run->mediaType == audio_MediaType) {
788 iPlayer *plr = audioPlayer_Media(media_GmDocument(d->doc), run->mediaId); 800 iPlayer *plr = audioPlayer_Media(media_GmDocument(d->doc), mediaId_GmRun(run));
789 if (idleTimeMs_Player(plr) > 3000 && ~flags_Player(plr) & volumeGrabbed_PlayerFlag && 801 if (idleTimeMs_Player(plr) > 3000 && ~flags_Player(plr) & volumeGrabbed_PlayerFlag &&
790 flags_Player(plr) & adjustingVolume_PlayerFlag) { 802 flags_Player(plr) & adjustingVolume_PlayerFlag) {
791 setFlags_Player(plr, adjustingVolume_PlayerFlag, iFalse); 803 setFlags_Player(plr, adjustingVolume_PlayerFlag, iFalse);
@@ -1244,6 +1256,9 @@ static const char *zipPageHeading_(const iRangecc mime) {
1244 if (equalCase_Rangecc(mime, "application/gpub+zip")) { 1256 if (equalCase_Rangecc(mime, "application/gpub+zip")) {
1245 return book_Icon " Gempub"; 1257 return book_Icon " Gempub";
1246 } 1258 }
1259 else if (equalCase_Rangecc(mime, mimeType_FontPack)) {
1260 return "\U0001f520 Fontpack";
1261 }
1247 iRangecc type = iNullRange; 1262 iRangecc type = iNullRange;
1248 nextSplit_Rangecc(mime, "/", &type); /* skip the part before the slash */ 1263 nextSplit_Rangecc(mime, "/", &type); /* skip the part before the slash */
1249 nextSplit_Rangecc(mime, "/", &type); 1264 nextSplit_Rangecc(mime, "/", &type);
@@ -1258,165 +1273,175 @@ static const char *zipPageHeading_(const iRangecc mime) {
1258 1273
1259static void postProcessRequestContent_DocumentWidget_(iDocumentWidget *d, iBool isCached) { 1274static void postProcessRequestContent_DocumentWidget_(iDocumentWidget *d, iBool isCached) {
1260 iWidget *w = as_Widget(d); 1275 iWidget *w = as_Widget(d);
1261 delete_Gempub(d->sourceGempub); 1276 /* Gempub page behavior and footer actions. */ {
1262 d->sourceGempub = NULL; 1277 /* TODO: move this to gempub.c */
1263 if (!cmpCase_String(&d->sourceMime, "application/octet-stream") || 1278 delete_Gempub(d->sourceGempub);
1264 !cmpCase_String(&d->sourceMime, mimeType_Gempub) || 1279 d->sourceGempub = NULL;
1265 endsWithCase_String(d->mod.url, ".gpub")) { 1280 if (!cmpCase_String(&d->sourceMime, "application/octet-stream") ||
1266 iGempub *gempub = new_Gempub(); 1281 !cmpCase_String(&d->sourceMime, mimeType_Gempub) ||
1267 if (open_Gempub(gempub, &d->sourceContent)) { 1282 endsWithCase_String(d->mod.url, ".gpub")) {
1268 setBaseUrl_Gempub(gempub, d->mod.url);
1269 setSource_DocumentWidget(d, collect_String(coverPageSource_Gempub(gempub)));
1270 setCStr_String(&d->sourceMime, mimeType_Gempub);
1271 d->sourceGempub = gempub;
1272 }
1273 else {
1274 delete_Gempub(gempub);
1275 }
1276 }
1277 if (!d->sourceGempub) {
1278 const iString *localPath = collect_String(localFilePathFromUrl_String(d->mod.url));
1279 iBool isInside = iFalse;
1280 if (localPath && !fileExists_FileInfo(localPath)) {
1281 /* This URL may refer to a file inside the archive. */
1282 localPath = findContainerArchive_Path(localPath);
1283 isInside = iTrue;
1284 }
1285 if (localPath && equal_CStr(mediaType_Path(localPath), "application/gpub+zip")) {
1286 iGempub *gempub = new_Gempub(); 1283 iGempub *gempub = new_Gempub();
1287 if (openFile_Gempub(gempub, localPath)) { 1284 if (open_Gempub(gempub, &d->sourceContent)) {
1288 setBaseUrl_Gempub(gempub, collect_String(makeFileUrl_String(localPath))); 1285 setBaseUrl_Gempub(gempub, d->mod.url);
1289 if (!isInside) { 1286 setSource_DocumentWidget(d, collect_String(coverPageSource_Gempub(gempub)));
1290 setSource_DocumentWidget(d, collect_String(coverPageSource_Gempub(gempub))); 1287 setCStr_String(&d->sourceMime, mimeType_Gempub);
1291 setCStr_String(&d->sourceMime, mimeType_Gempub);
1292 }
1293 d->sourceGempub = gempub; 1288 d->sourceGempub = gempub;
1294 } 1289 }
1295 else { 1290 else {
1296 delete_Gempub(gempub); 1291 delete_Gempub(gempub);
1297 } 1292 }
1298 } 1293 }
1299 } 1294 if (!d->sourceGempub) {
1300 if (d->sourceGempub) { 1295 const iString *localPath = collect_String(localFilePathFromUrl_String(d->mod.url));
1301 if (equal_String(d->mod.url, coverPageUrl_Gempub(d->sourceGempub))) { 1296 iBool isInside = iFalse;
1302 if (!isRemote_Gempub(d->sourceGempub)) { 1297 if (localPath && !fileExists_FileInfo(localPath)) {
1303 iArray *items = collectNew_Array(sizeof(iMenuItem)); 1298 /* This URL may refer to a file inside the archive. */
1304 pushBack_Array( 1299 localPath = findContainerArchive_Path(localPath);
1305 items, 1300 isInside = iTrue;
1306 &(iMenuItem){ book_Icon " ${gempub.cover.view}",
1307 0,
1308 0,
1309 format_CStr("!open url:%s",
1310 cstr_String(indexPageUrl_Gempub(d->sourceGempub))) });
1311 if (navSize_Gempub(d->sourceGempub) > 0) {
1312 pushBack_Array(
1313 items,
1314 &(iMenuItem){
1315 format_CStr(forwardArrow_Icon " %s",
1316 cstr_String(navLinkLabel_Gempub(d->sourceGempub, 0))),
1317 SDLK_RIGHT,
1318 0,
1319 format_CStr("!open url:%s",
1320 cstr_String(navLinkUrl_Gempub(d->sourceGempub, 0))) });
1321 }
1322 makeFooterButtons_DocumentWidget_(d, constData_Array(items), size_Array(items));
1323 } 1301 }
1324 else { 1302 if (localPath && equal_CStr(mediaType_Path(localPath), mimeType_Gempub)) {
1325 makeFooterButtons_DocumentWidget_( 1303 iGempub *gempub = new_Gempub();
1326 d, 1304 if (openFile_Gempub(gempub, localPath)) {
1327 (iMenuItem[]){ { book_Icon " ${menu.save.downloads.open}", 1305 setBaseUrl_Gempub(gempub, collect_String(makeFileUrl_String(localPath)));
1328 SDLK_s, 1306 if (!isInside) {
1329 KMOD_PRIMARY | KMOD_SHIFT, 1307 setSource_DocumentWidget(d, collect_String(coverPageSource_Gempub(gempub)));
1330 "document.save open:1" }, 1308 setCStr_String(&d->sourceMime, mimeType_Gempub);
1331 { download_Icon " " saveToDownloads_Label, 1309 }
1332 SDLK_s, 1310 d->sourceGempub = gempub;
1333 KMOD_PRIMARY, 1311 }
1334 "document.save" } }, 1312 else {
1335 2); 1313 delete_Gempub(gempub);
1336 } 1314 }
1337 if (preloadCoverImage_Gempub(d->sourceGempub, d->doc)) {
1338 redoLayout_GmDocument(d->doc);
1339 updateVisible_DocumentWidget_(d);
1340 invalidate_DocumentWidget_(d);
1341 } 1315 }
1342 } 1316 }
1343 else if (equal_String(d->mod.url, indexPageUrl_Gempub(d->sourceGempub))) { 1317 if (d->sourceGempub) {
1344 makeFooterButtons_DocumentWidget_( 1318 if (equal_String(d->mod.url, coverPageUrl_Gempub(d->sourceGempub))) {
1345 d, 1319 if (!isRemote_Gempub(d->sourceGempub)) {
1346 (iMenuItem[]){ { format_CStr(book_Icon " %s", 1320 iArray *items = collectNew_Array(sizeof(iMenuItem));
1347 cstr_String(property_Gempub(d->sourceGempub,
1348 title_GempubProperty))),
1349 SDLK_LEFT,
1350 0,
1351 format_CStr("!open url:%s",
1352 cstr_String(coverPageUrl_Gempub(d->sourceGempub))) } },
1353 1);
1354 }
1355 else {
1356 /* Navigation buttons. */
1357 iArray *items = collectNew_Array(sizeof(iMenuItem));
1358 const size_t navIndex = navIndex_Gempub(d->sourceGempub, d->mod.url);
1359 if (navIndex != iInvalidPos) {
1360 if (navIndex < navSize_Gempub(d->sourceGempub) - 1) {
1361 pushBack_Array( 1321 pushBack_Array(
1362 items, 1322 items,
1363 &(iMenuItem){ 1323 &(iMenuItem){ book_Icon " ${gempub.cover.view}",
1364 format_CStr(forwardArrow_Icon " %s", 1324 0,
1365 cstr_String(navLinkLabel_Gempub(d->sourceGempub, navIndex + 1))), 1325 0,
1366 SDLK_RIGHT, 1326 format_CStr("!open url:%s",
1367 0, 1327 cstr_String(indexPageUrl_Gempub(d->sourceGempub))) });
1368 format_CStr("!open url:%s", 1328 if (navSize_Gempub(d->sourceGempub) > 0) {
1369 cstr_String(navLinkUrl_Gempub(d->sourceGempub, navIndex + 1))) }); 1329 pushBack_Array(
1330 items,
1331 &(iMenuItem){
1332 format_CStr(forwardArrow_Icon " %s",
1333 cstr_String(navLinkLabel_Gempub(d->sourceGempub, 0))),
1334 SDLK_RIGHT,
1335 0,
1336 format_CStr("!open url:%s",
1337 cstr_String(navLinkUrl_Gempub(d->sourceGempub, 0))) });
1338 }
1339 makeFooterButtons_DocumentWidget_(d, constData_Array(items), size_Array(items));
1370 } 1340 }
1371 if (navIndex > 0) { 1341 else {
1372 pushBack_Array( 1342 makeFooterButtons_DocumentWidget_(
1373 items, 1343 d,
1374 &(iMenuItem){ 1344 (iMenuItem[]){ { book_Icon " ${menu.save.downloads.open}",
1375 format_CStr(backArrow_Icon " %s", 1345 SDLK_s,
1376 cstr_String(navLinkLabel_Gempub(d->sourceGempub, navIndex - 1))), 1346 KMOD_PRIMARY | KMOD_SHIFT,
1377 SDLK_LEFT, 1347 "document.save open:1" },
1378 0, 1348 { download_Icon " " saveToDownloads_Label,
1379 format_CStr("!open url:%s", 1349 SDLK_s,
1380 cstr_String(navLinkUrl_Gempub(d->sourceGempub, navIndex - 1))) }); 1350 KMOD_PRIMARY,
1351 "document.save" } },
1352 2);
1381 } 1353 }
1382 else if (!equalCase_String(d->mod.url, indexPageUrl_Gempub(d->sourceGempub))) { 1354 if (preloadCoverImage_Gempub(d->sourceGempub, d->doc)) {
1383 pushBack_Array( 1355 redoLayout_GmDocument(d->doc);
1384 items, 1356 updateVisible_DocumentWidget_(d);
1385 &(iMenuItem){ 1357 invalidate_DocumentWidget_(d);
1386 format_CStr(book_Icon " %s",
1387 cstr_String(property_Gempub(d->sourceGempub, title_GempubProperty))),
1388 SDLK_LEFT,
1389 0,
1390 format_CStr("!open url:%s",
1391 cstr_String(coverPageUrl_Gempub(d->sourceGempub))) });
1392 } 1358 }
1393 } 1359 }
1394 if (!isEmpty_Array(items)) { 1360 else if (equal_String(d->mod.url, indexPageUrl_Gempub(d->sourceGempub))) {
1395 makeFooterButtons_DocumentWidget_(d, constData_Array(items), size_Array(items)); 1361 makeFooterButtons_DocumentWidget_(
1362 d,
1363 (iMenuItem[]){ { format_CStr(book_Icon " %s",
1364 cstr_String(property_Gempub(d->sourceGempub,
1365 title_GempubProperty))),
1366 SDLK_LEFT,
1367 0,
1368 format_CStr("!open url:%s",
1369 cstr_String(coverPageUrl_Gempub(d->sourceGempub))) } },
1370 1);
1396 } 1371 }
1397 } 1372 else {
1398 if (!isCached && prefs_App()->pinSplit && 1373 /* Navigation buttons. */
1399 equal_String(d->mod.url, indexPageUrl_Gempub(d->sourceGempub))) { 1374 iArray *items = collectNew_Array(sizeof(iMenuItem));
1400 const iString *navStart = navStartLinkUrl_Gempub(d->sourceGempub); 1375 const size_t navIndex = navIndex_Gempub(d->sourceGempub, d->mod.url);
1401 if (navStart) { 1376 if (navIndex != iInvalidPos) {
1402 iWindow *win = get_Window(); 1377 if (navIndex < navSize_Gempub(d->sourceGempub) - 1) {
1403 /* Auto-split to show index and the first navigation link. */ 1378 pushBack_Array(
1404 if (numRoots_Window(win) == 2) { 1379 items,
1405 /* This document is showing the index page. */ 1380 &(iMenuItem){
1406 iRoot *other = otherRoot_Window(win, w->root); 1381 format_CStr(forwardArrow_Icon " %s",
1407 postCommandf_Root(other, "open url:%s", cstr_String(navStart)); 1382 cstr_String(navLinkLabel_Gempub(d->sourceGempub, navIndex + 1))),
1408 if (prefs_App()->pinSplit == 1 && w->root == win->roots[1]) { 1383 SDLK_RIGHT,
1409 /* On the wrong side. */ 1384 0,
1410 postCommand_App("ui.split swap:1"); 1385 format_CStr("!open url:%s",
1386 cstr_String(navLinkUrl_Gempub(d->sourceGempub, navIndex + 1))) });
1387 }
1388 if (navIndex > 0) {
1389 pushBack_Array(
1390 items,
1391 &(iMenuItem){
1392 format_CStr(backArrow_Icon " %s",
1393 cstr_String(navLinkLabel_Gempub(d->sourceGempub, navIndex - 1))),
1394 SDLK_LEFT,
1395 0,
1396 format_CStr("!open url:%s",
1397 cstr_String(navLinkUrl_Gempub(d->sourceGempub, navIndex - 1))) });
1398 }
1399 else if (!equalCase_String(d->mod.url, indexPageUrl_Gempub(d->sourceGempub))) {
1400 pushBack_Array(
1401 items,
1402 &(iMenuItem){
1403 format_CStr(book_Icon " %s",
1404 cstr_String(property_Gempub(d->sourceGempub, title_GempubProperty))),
1405 SDLK_LEFT,
1406 0,
1407 format_CStr("!open url:%s",
1408 cstr_String(coverPageUrl_Gempub(d->sourceGempub))) });
1411 } 1409 }
1412 } 1410 }
1413 else { 1411 if (!isEmpty_Array(items)) {
1414 postCommandf_App( 1412 makeFooterButtons_DocumentWidget_(d, constData_Array(items), size_Array(items));
1415 "open newtab:%d url:%s", otherRoot_OpenTabFlag, cstr_String(navStart)); 1413 }
1414 }
1415 if (!isCached && prefs_App()->pinSplit &&
1416 equal_String(d->mod.url, indexPageUrl_Gempub(d->sourceGempub))) {
1417 const iString *navStart = navStartLinkUrl_Gempub(d->sourceGempub);
1418 if (navStart) {
1419 iWindow *win = get_Window();
1420 /* Auto-split to show index and the first navigation link. */
1421 if (numRoots_Window(win) == 2) {
1422 /* This document is showing the index page. */
1423 iRoot *other = otherRoot_Window(win, w->root);
1424 postCommandf_Root(other, "open url:%s", cstr_String(navStart));
1425 if (prefs_App()->pinSplit == 1 && w->root == win->roots[1]) {
1426 /* On the wrong side. */
1427 postCommand_App("ui.split swap:1");
1428 }
1429 }
1430 else {
1431 postCommandf_App(
1432 "open newtab:%d url:%s", otherRoot_OpenTabFlag, cstr_String(navStart));
1433 }
1416 } 1434 }
1417 } 1435 }
1418 } 1436 }
1419 } 1437 }
1438 /* Local fontpacks are automatically shown. */
1439 if (preloadLocalFontpackForPreview_Fonts(d->doc)) {
1440 documentRunsInvalidated_DocumentWidget_(d);
1441 redoLayout_GmDocument(d->doc);
1442 updateVisible_DocumentWidget_(d);
1443 invalidate_DocumentWidget_(d);
1444 }
1420} 1445}
1421 1446
1422static void updateDocument_DocumentWidget_(iDocumentWidget *d, 1447static void updateDocument_DocumentWidget_(iDocumentWidget *d,
@@ -1484,7 +1509,7 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d,
1484 } 1509 }
1485 delete_String(localPath); 1510 delete_String(localPath);
1486 if (equalCase_Rangecc(urlScheme_String(d->mod.url), "file")) { 1511 if (equalCase_Rangecc(urlScheme_String(d->mod.url), "file")) {
1487 appendFormat_String(&str, "=> %s/ ${doc.archive.view}\n", 1512 appendFormat_String(&str, "=> %s/ " folder_Icon " ${doc.archive.view}\n",
1488 cstr_String(withSpacesEncoded_String(d->mod.url))); 1513 cstr_String(withSpacesEncoded_String(d->mod.url)));
1489 } 1514 }
1490 translate_Lang(&str); 1515 translate_Lang(&str);
@@ -2089,7 +2114,7 @@ static iBool requestMedia_DocumentWidget_(iDocumentWidget *d, iGmLinkId linkId,
2089} 2114}
2090 2115
2091static iBool isDownloadRequest_DocumentWidget(const iDocumentWidget *d, const iMediaRequest *req) { 2116static iBool isDownloadRequest_DocumentWidget(const iDocumentWidget *d, const iMediaRequest *req) {
2092 return findLinkDownload_Media(constMedia_GmDocument(d->doc), req->linkId) != 0; 2117 return findMediaForLink_Media(constMedia_GmDocument(d->doc), req->linkId, download_MediaType).type != 0;
2093} 2118}
2094 2119
2095static iBool handleMediaCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) { 2120static iBool handleMediaCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) {
@@ -2174,7 +2199,7 @@ static void allocVisBuffer_DocumentWidget_(const iDocumentWidget *d) {
2174static iBool fetchNextUnfetchedImage_DocumentWidget_(iDocumentWidget *d) { 2199static iBool fetchNextUnfetchedImage_DocumentWidget_(iDocumentWidget *d) {
2175 iConstForEach(PtrArray, i, &d->visibleLinks) { 2200 iConstForEach(PtrArray, i, &d->visibleLinks) {
2176 const iGmRun *run = i.ptr; 2201 const iGmRun *run = i.ptr;
2177 if (run->linkId && run->mediaType == none_GmRunMediaType && 2202 if (run->linkId && run->mediaType == none_MediaType &&
2178 ~run->flags & decoration_GmRunFlag) { 2203 ~run->flags & decoration_GmRunFlag) {
2179 const int linkFlags = linkFlags_GmDocument(d->doc, run->linkId); 2204 const int linkFlags = linkFlags_GmDocument(d->doc, run->linkId);
2180 if (isMediaLink_GmDocument(d->doc, run->linkId) && 2205 if (isMediaLink_GmDocument(d->doc, run->linkId) &&
@@ -2763,8 +2788,10 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
2763 else if (equalWidget_Command(cmd, w, "document.downloadlink")) { 2788 else if (equalWidget_Command(cmd, w, "document.downloadlink")) {
2764 if (d->contextLink) { 2789 if (d->contextLink) {
2765 const iGmLinkId linkId = d->contextLink->linkId; 2790 const iGmLinkId linkId = d->contextLink->linkId;
2766 setDownloadUrl_Media( 2791 setUrl_Media(media_GmDocument(d->doc),
2767 media_GmDocument(d->doc), linkId, linkUrl_GmDocument(d->doc, linkId)); 2792 linkId,
2793 download_MediaType,
2794 linkUrl_GmDocument(d->doc, linkId));
2768 requestMedia_DocumentWidget_(d, linkId, iFalse /* no filters */); 2795 requestMedia_DocumentWidget_(d, linkId, iFalse /* no filters */);
2769 redoLayout_GmDocument(d->doc); /* inline downloader becomes visible */ 2796 redoLayout_GmDocument(d->doc); /* inline downloader becomes visible */
2770 updateVisible_DocumentWidget_(d); 2797 updateVisible_DocumentWidget_(d);
@@ -2874,7 +2901,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
2874 const iMedia * media = media_GmDocument(d->doc); 2901 const iMedia * media = media_GmDocument(d->doc);
2875 const size_t num = numAudio_Media(media); 2902 const size_t num = numAudio_Media(media);
2876 for (size_t id = 1; id <= num; id++) { 2903 for (size_t id = 1; id <= num; id++) {
2877 iPlayer *plr = audioPlayer_Media(media, id); 2904 iPlayer *plr = audioPlayer_Media(media, (iMediaId){ audio_MediaType, id });
2878 if (plr != startedPlr) { 2905 if (plr != startedPlr) {
2879 setPaused_Player(plr, iTrue); 2906 setPaused_Player(plr, iTrue);
2880 } 2907 }
@@ -3212,8 +3239,8 @@ static iRect runRect_DocumentWidget_(const iDocumentWidget *d, const iGmRun *run
3212} 3239}
3213 3240
3214static void setGrabbedPlayer_DocumentWidget_(iDocumentWidget *d, const iGmRun *run) { 3241static void setGrabbedPlayer_DocumentWidget_(iDocumentWidget *d, const iGmRun *run) {
3215 if (run && run->mediaType == audio_GmRunMediaType) { 3242 if (run && run->mediaType == audio_MediaType) {
3216 iPlayer *plr = audioPlayer_Media(media_GmDocument(d->doc), run->mediaId); 3243 iPlayer *plr = audioPlayer_Media(media_GmDocument(d->doc), mediaId_GmRun(run));
3217 setFlags_Player(plr, volumeGrabbed_PlayerFlag, iTrue); 3244 setFlags_Player(plr, volumeGrabbed_PlayerFlag, iTrue);
3218 d->grabbedStartVolume = volume_Player(plr); 3245 d->grabbedStartVolume = volume_Player(plr);
3219 d->grabbedPlayer = run; 3246 d->grabbedPlayer = run;
@@ -3221,7 +3248,7 @@ static void setGrabbedPlayer_DocumentWidget_(iDocumentWidget *d, const iGmRun *r
3221 } 3248 }
3222 else if (d->grabbedPlayer) { 3249 else if (d->grabbedPlayer) {
3223 setFlags_Player( 3250 setFlags_Player(
3224 audioPlayer_Media(media_GmDocument(d->doc), d->grabbedPlayer->mediaId), 3251 audioPlayer_Media(media_GmDocument(d->doc), mediaId_GmRun(d->grabbedPlayer)),
3225 volumeGrabbed_PlayerFlag, 3252 volumeGrabbed_PlayerFlag,
3226 iFalse); 3253 iFalse);
3227 d->grabbedPlayer = NULL; 3254 d->grabbedPlayer = NULL;
@@ -3249,11 +3276,21 @@ static iBool processMediaEvents_DocumentWidget_(iDocumentWidget *d, const SDL_Ev
3249 const iInt2 mouse = init_I2(ev->button.x, ev->button.y); 3276 const iInt2 mouse = init_I2(ev->button.x, ev->button.y);
3250 iConstForEach(PtrArray, i, &d->visibleMedia) { 3277 iConstForEach(PtrArray, i, &d->visibleMedia) {
3251 const iGmRun *run = i.ptr; 3278 const iGmRun *run = i.ptr;
3252 if (run->mediaType != audio_GmRunMediaType) { 3279 if (run->mediaType == fontpack_MediaType) {
3280 iFontpackUI ui;
3281 init_FontpackUI(&ui, media_GmDocument(d->doc), run->mediaId,
3282 runRect_DocumentWidget_(d, run));
3283 if (processEvent_FontpackUI(&ui, ev)) {
3284 refresh_Widget(d);
3285 return iTrue;
3286 }
3287 }
3288 if (run->mediaType != audio_MediaType) {
3253 continue; 3289 continue;
3254 } 3290 }
3291 /* TODO: move this to mediaui.c */
3255 const iRect rect = runRect_DocumentWidget_(d, run); 3292 const iRect rect = runRect_DocumentWidget_(d, run);
3256 iPlayer * plr = audioPlayer_Media(media_GmDocument(d->doc), run->mediaId); 3293 iPlayer * plr = audioPlayer_Media(media_GmDocument(d->doc), mediaId_GmRun(run));
3257 if (contains_Rect(rect, mouse)) { 3294 if (contains_Rect(rect, mouse)) {
3258 iPlayerUI ui; 3295 iPlayerUI ui;
3259 init_PlayerUI(&ui, plr, rect); 3296 init_PlayerUI(&ui, plr, rect);
@@ -3633,7 +3670,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
3633 cstr_String(linkUrl)) }, 3670 cstr_String(linkUrl)) },
3634 }, 3671 },
3635 3); 3672 3);
3636 if (isNative && d->contextLink->mediaType != download_GmRunMediaType) { 3673 if (isNative && d->contextLink->mediaType != download_MediaType) {
3637 pushBackN_Array(&items, (iMenuItem[]){ 3674 pushBackN_Array(&items, (iMenuItem[]){
3638 { "---" }, 3675 { "---" },
3639 { download_Icon " ${link.download}", 0, 0, "document.downloadlink" }, 3676 { download_Icon " ${link.download}", 0, 0, "document.downloadlink" },
@@ -3641,7 +3678,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
3641 } 3678 }
3642 iMediaRequest *mediaReq; 3679 iMediaRequest *mediaReq;
3643 if ((mediaReq = findMediaRequest_DocumentWidget_(d, d->contextLink->linkId)) != NULL && 3680 if ((mediaReq = findMediaRequest_DocumentWidget_(d, d->contextLink->linkId)) != NULL &&
3644 d->contextLink->mediaType != download_GmRunMediaType) { 3681 d->contextLink->mediaType != download_MediaType) {
3645 if (isFinished_GmRequest(mediaReq->req)) { 3682 if (isFinished_GmRequest(mediaReq->req)) {
3646 pushBack_Array(&items, 3683 pushBack_Array(&items,
3647 &(iMenuItem){ download_Icon " " saveToDownloads_Label, 3684 &(iMenuItem){ download_Icon " " saveToDownloads_Label,
@@ -3763,7 +3800,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
3763 case drag_ClickResult: { 3800 case drag_ClickResult: {
3764 if (d->grabbedPlayer) { 3801 if (d->grabbedPlayer) {
3765 iPlayer *plr = 3802 iPlayer *plr =
3766 audioPlayer_Media(media_GmDocument(d->doc), d->grabbedPlayer->mediaId); 3803 audioPlayer_Media(media_GmDocument(d->doc), mediaId_GmRun(d->grabbedPlayer));
3767 iPlayerUI ui; 3804 iPlayerUI ui;
3768 init_PlayerUI(&ui, plr, runRect_DocumentWidget_(d, d->grabbedPlayer)); 3805 init_PlayerUI(&ui, plr, runRect_DocumentWidget_(d, d->grabbedPlayer));
3769 float off = (float) delta_Click(&d->click).x / (float) width_Rect(ui.volumeSlider); 3806 float off = (float) delta_Click(&d->click).x / (float) width_Rect(ui.volumeSlider);
@@ -3883,8 +3920,9 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
3883 } 3920 }
3884 if (d->hoverLink) { 3921 if (d->hoverLink) {
3885 /* TODO: Move this to a method. */ 3922 /* TODO: Move this to a method. */
3886 const iGmLinkId linkId = d->hoverLink->linkId; 3923 const iGmLinkId linkId = d->hoverLink->linkId;
3887 const int linkFlags = linkFlags_GmDocument(d->doc, linkId); 3924 const iMediaId linkMedia = mediaId_GmRun(d->hoverLink);
3925 const int linkFlags = linkFlags_GmDocument(d->doc, linkId);
3888 iAssert(linkId); 3926 iAssert(linkId);
3889 /* Media links are opened inline by default. */ 3927 /* Media links are opened inline by default. */
3890 if (isMediaLink_GmDocument(d->doc, linkId)) { 3928 if (isMediaLink_GmDocument(d->doc, linkId)) {
@@ -3937,6 +3975,12 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
3937 } 3975 }
3938 refresh_Widget(w); 3976 refresh_Widget(w);
3939 } 3977 }
3978 else if (linkMedia.type == download_MediaType ||
3979 findMediaRequest_DocumentWidget_(d, linkId)) {
3980 /* TODO: What should be done when clicking on an inline download?
3981 Maybe dismiss if finished? */
3982 return iTrue;
3983 }
3940 else if (linkFlags & supportedScheme_GmLinkFlag) { 3984 else if (linkFlags & supportedScheme_GmLinkFlag) {
3941 int tabMode = openTabMode_Sym(modState_Keys()); 3985 int tabMode = openTabMode_Sym(modState_Keys());
3942 if (isPinned_DocumentWidget_(d)) { 3986 if (isPinned_DocumentWidget_(d)) {
@@ -4071,7 +4115,7 @@ static void fillRange_DrawContext_(iDrawContext *d, const iGmRun *run, enum iCol
4071 4115
4072static void drawMark_DrawContext_(void *context, const iGmRun *run) { 4116static void drawMark_DrawContext_(void *context, const iGmRun *run) {
4073 iDrawContext *d = context; 4117 iDrawContext *d = context;
4074 if (run->mediaType == none_GmRunMediaType) { 4118 if (run->mediaType == none_MediaType) {
4075 fillRange_DrawContext_(d, run, uiMatching_ColorId, d->widget->foundMark, &d->inFoundMark); 4119 fillRange_DrawContext_(d, run, uiMatching_ColorId, d->widget->foundMark, &d->inFoundMark);
4076 fillRange_DrawContext_(d, run, uiMarked_ColorId, d->widget->selectMark, &d->inSelectMark); 4120 fillRange_DrawContext_(d, run, uiMarked_ColorId, d->widget->selectMark, &d->inSelectMark);
4077 } 4121 }
@@ -4178,8 +4222,8 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
4178 d->runsDrawn.end = run; 4222 d->runsDrawn.end = run;
4179 } 4223 }
4180 } 4224 }
4181 if (run->mediaType == image_GmRunMediaType) { 4225 if (run->mediaType == image_MediaType) {
4182 SDL_Texture *tex = imageTexture_Media(media_GmDocument(d->widget->doc), run->mediaId); 4226 SDL_Texture *tex = imageTexture_Media(media_GmDocument(d->widget->doc), mediaId_GmRun(run));
4183 const iRect dst = moved_Rect(run->visBounds, origin); 4227 const iRect dst = moved_Rect(run->visBounds, origin);
4184 if (tex) { 4228 if (tex) {
4185 fillRect_Paint(&d->paint, dst, tmBackground_ColorId); /* in case the image has alpha */ 4229 fillRect_Paint(&d->paint, dst, tmBackground_ColorId); /* in case the image has alpha */
@@ -4334,31 +4378,31 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
4334 fg = linkColor_GmDocument(doc, run->linkId, textHover_GmLinkPart); 4378 fg = linkColor_GmDocument(doc, run->linkId, textHover_GmLinkPart);
4335 iString text; 4379 iString text;
4336 init_String(&text); 4380 init_String(&text);
4337 iMediaId imageId = linkImage_GmDocument(doc, run->linkId); 4381 const iMediaId linkMedia = findMediaForLink_Media(constMedia_GmDocument(doc),
4338 iMediaId audioId = !imageId ? linkAudio_GmDocument(doc, run->linkId) : 0; 4382 run->linkId, none_MediaType);
4339 iMediaId downloadId = !imageId && !audioId ? 4383 iAssert(linkMedia.type != none_MediaType);
4340 findLinkDownload_Media(constMedia_GmDocument(doc), run->linkId) : 0; 4384 iGmMediaInfo info;
4341 iAssert(imageId || audioId || downloadId); 4385 info_Media(constMedia_GmDocument(doc), linkMedia, &info);
4342 if (imageId) { 4386 switch (linkMedia.type) {
4343 iAssert(!isEmpty_Rect(run->bounds)); 4387 case image_MediaType: {
4344 iGmMediaInfo info; 4388 iAssert(!isEmpty_Rect(run->bounds));
4345 imageInfo_Media(constMedia_GmDocument(doc), imageId, &info); 4389 const iInt2 imgSize = imageSize_Media(constMedia_GmDocument(doc), linkMedia);
4346 const iInt2 imgSize = imageSize_Media(constMedia_GmDocument(doc), imageId); 4390 format_String(&text, "%s \u2014 %d x %d \u2014 %.1f%s",
4347 format_String(&text, "%s \u2014 %d x %d \u2014 %.1f%s", 4391 info.type, imgSize.x, imgSize.y, info.numBytes / 1.0e6f,
4348 info.type, imgSize.x, imgSize.y, info.numBytes / 1.0e6f, 4392 cstr_Lang("mb"));
4349 cstr_Lang("mb")); 4393 break;
4350 } 4394 }
4351 else if (audioId) { 4395 case audio_MediaType:
4352 iGmMediaInfo info; 4396 format_String(&text, "%s", info.type);
4353 audioInfo_Media(constMedia_GmDocument(doc), audioId, &info); 4397 break;
4354 format_String(&text, "%s", info.type); 4398 case download_MediaType:
4355 } 4399 format_String(&text, "%s", info.type);
4356 else if (downloadId) { 4400 break;
4357 iGmMediaInfo info; 4401 default:
4358 downloadInfo_Media(constMedia_GmDocument(doc), downloadId, &info); 4402 break;
4359 format_String(&text, "%s", info.type);
4360 } 4403 }
4361 if (findMediaRequest_DocumentWidget_(d->widget, run->linkId)) { 4404 if (linkMedia.type != download_MediaType && /* can't cancel downloads currently */
4405 findMediaRequest_DocumentWidget_(d->widget, run->linkId)) {
4362 appendFormat_String( 4406 appendFormat_String(
4363 &text, " %s" close_Icon, isHover ? escape_Color(tmLinkText_ColorId) : ""); 4407 &text, " %s" close_Icon, isHover ? escape_Color(tmLinkText_ColorId) : "");
4364 } 4408 }
@@ -4589,18 +4633,25 @@ static void drawSideElements_DocumentWidget_(const iDocumentWidget *d) {
4589static void drawMedia_DocumentWidget_(const iDocumentWidget *d, iPaint *p) { 4633static void drawMedia_DocumentWidget_(const iDocumentWidget *d, iPaint *p) {
4590 iConstForEach(PtrArray, i, &d->visibleMedia) { 4634 iConstForEach(PtrArray, i, &d->visibleMedia) {
4591 const iGmRun * run = i.ptr; 4635 const iGmRun * run = i.ptr;
4592 if (run->mediaType == audio_GmRunMediaType) { 4636 if (run->mediaType == audio_MediaType) {
4593 iPlayerUI ui; 4637 iPlayerUI ui;
4594 init_PlayerUI(&ui, 4638 init_PlayerUI(&ui,
4595 audioPlayer_Media(media_GmDocument(d->doc), run->mediaId), 4639 audioPlayer_Media(media_GmDocument(d->doc), mediaId_GmRun(run)),
4596 runRect_DocumentWidget_(d, run)); 4640 runRect_DocumentWidget_(d, run));
4597 draw_PlayerUI(&ui, p); 4641 draw_PlayerUI(&ui, p);
4598 } 4642 }
4599 else if (run->mediaType == download_GmRunMediaType) { 4643 else if (run->mediaType == download_MediaType) {
4600 iDownloadUI ui; 4644 iDownloadUI ui;
4601 init_DownloadUI(&ui, d, run->mediaId, runRect_DocumentWidget_(d, run)); 4645 init_DownloadUI(&ui, constMedia_GmDocument(d->doc), run->mediaId,
4646 runRect_DocumentWidget_(d, run));
4602 draw_DownloadUI(&ui, p); 4647 draw_DownloadUI(&ui, p);
4603 } 4648 }
4649 else if (run->mediaType == fontpack_MediaType) {
4650 iFontpackUI ui;
4651 init_FontpackUI(&ui, constMedia_GmDocument(d->doc), run->mediaId,
4652 runRect_DocumentWidget_(d, run));
4653 draw_FontpackUI(&ui, p);
4654 }
4604 } 4655 }
4605} 4656}
4606 4657
diff --git a/src/ui/documentwidget.h b/src/ui/documentwidget.h
index cc09c72d..1f2ecfc0 100644
--- a/src/ui/documentwidget.h
+++ b/src/ui/documentwidget.h
@@ -32,6 +32,8 @@ iDeclareType(History)
32iDeclareWidgetClass(DocumentWidget) 32iDeclareWidgetClass(DocumentWidget)
33iDeclareObjectConstruction(DocumentWidget) 33iDeclareObjectConstruction(DocumentWidget)
34 34
35void cancelAllRequests_DocumentWidget(iDocumentWidget *);
36
35void serializeState_DocumentWidget (const iDocumentWidget *, iStream *outs); 37void serializeState_DocumentWidget (const iDocumentWidget *, iStream *outs);
36void deserializeState_DocumentWidget (iDocumentWidget *, iStream *ins); 38void deserializeState_DocumentWidget (iDocumentWidget *, iStream *ins);
37 39
diff --git a/src/ui/mediaui.c b/src/ui/mediaui.c
index 22552027..aa45d73a 100644
--- a/src/ui/mediaui.c
+++ b/src/ui/mediaui.c
@@ -30,6 +30,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
30#include "lang.h" 30#include "lang.h"
31 31
32#include <the_Foundation/path.h> 32#include <the_Foundation/path.h>
33#include <the_Foundation/stringlist.h>
33 34
34static const char *volumeChar_(float volume) { 35static const char *volumeChar_(float volume) {
35 if (volume <= 0) { 36 if (volume <= 0) {
@@ -61,7 +62,7 @@ void init_PlayerUI(iPlayerUI *d, const iPlayer *player, iRect bounds) {
61 } 62 }
62} 63}
63 64
64static void drawPlayerButton_(iPaint *p, iRect rect, const char *label, int font) { 65static void drawInlineButton_(iPaint *p, iRect rect, const char *label, int font) {
65 const iInt2 mouse = mouseCoord_Window(get_Window(), 0); 66 const iInt2 mouse = mouseCoord_Window(get_Window(), 0);
66 const iBool isHover = contains_Rect(rect, mouse); 67 const iBool isHover = contains_Rect(rect, mouse);
67 const iBool isPressed = isHover && (SDL_GetMouseState(NULL, NULL) & SDL_BUTTON_LEFT) != 0; 68 const iBool isPressed = isHover && (SDL_GetMouseState(NULL, NULL) & SDL_BUTTON_LEFT) != 0;
@@ -113,14 +114,14 @@ void draw_PlayerUI(iPlayerUI *d, iPaint *p) {
113 const iBool isAdjusting = (flags_Player(d->player) & adjustingVolume_PlayerFlag) != 0; 114 const iBool isAdjusting = (flags_Player(d->player) & adjustingVolume_PlayerFlag) != 0;
114 fillRect_Paint(p, d->bounds, playerBackground_ColorId); 115 fillRect_Paint(p, d->bounds, playerBackground_ColorId);
115 drawRect_Paint(p, d->bounds, playerFrame_ColorId); 116 drawRect_Paint(p, d->bounds, playerFrame_ColorId);
116 drawPlayerButton_(p, 117 drawInlineButton_(p,
117 d->playPauseRect, 118 d->playPauseRect,
118 isPaused_Player(d->player) ? "\U0001f782" : "\u23f8", 119 isPaused_Player(d->player) ? "\U0001f782" : "\u23f8",
119 uiContent_FontId); 120 uiContent_FontId);
120 drawPlayerButton_(p, d->rewindRect, "\u23ee", uiContent_FontId); 121 drawInlineButton_(p, d->rewindRect, "\u23ee", uiContent_FontId);
121 drawPlayerButton_(p, d->menuRect, menu_Icon, uiContent_FontId); 122 drawInlineButton_(p, d->menuRect, menu_Icon, uiContent_FontId);
122 if (!isAdjusting) { 123 if (!isAdjusting) {
123 drawPlayerButton_( 124 drawInlineButton_(
124 p, d->volumeRect, volumeChar_(volume_Player(d->player)), uiContentSymbols_FontId); 125 p, d->volumeRect, volumeChar_(volume_Player(d->player)), uiContentSymbols_FontId);
125 } 126 }
126 const int hgt = lineHeight_Text(uiLabelBig_FontId); 127 const int hgt = lineHeight_Text(uiLabelBig_FontId);
@@ -228,24 +229,26 @@ static void drawSevenSegmentBytes_(iInt2 pos, int color, size_t numBytes) {
228 deinit_String(&digits); 229 deinit_String(&digits);
229} 230}
230 231
231void init_DownloadUI(iDownloadUI *d, const iDocumentWidget *doc, uint16_t mediaId, iRect bounds) { 232void init_DownloadUI(iDownloadUI *d, const iMedia *media, uint16_t mediaId, iRect bounds) {
232 d->doc = doc; 233 d->media = media;
233 d->mediaId = mediaId; 234 d->mediaId = mediaId;
234 d->bounds = bounds; 235 d->bounds = bounds;
235} 236}
236 237
238/*----------------------------------------------------------------------------------------------*/
239
237iBool processEvent_DownloadUI(iDownloadUI *d, const SDL_Event *ev) { 240iBool processEvent_DownloadUI(iDownloadUI *d, const SDL_Event *ev) {
238 return iFalse; 241 return iFalse;
239} 242}
240 243
241void draw_DownloadUI(const iDownloadUI *d, iPaint *p) { 244void draw_DownloadUI(const iDownloadUI *d, iPaint *p) {
242 const iMedia *media = constMedia_GmDocument(document_DocumentWidget(d->doc));
243 iGmMediaInfo info; 245 iGmMediaInfo info;
244 float bytesPerSecond; 246 float bytesPerSecond;
245 const iString *path; 247 const iString *path;
246 iBool isFinished; 248 iBool isFinished;
247 downloadInfo_Media(media, d->mediaId, &info); 249 downloadInfo_Media(d->media, d->mediaId, &info);
248 downloadStats_Media(media, d->mediaId, &path, &bytesPerSecond, &isFinished); 250 downloadStats_Media(d->media, (iMediaId){ download_MediaType, d->mediaId },
251 &path, &bytesPerSecond, &isFinished);
249 fillRect_Paint(p, d->bounds, uiBackground_ColorId); 252 fillRect_Paint(p, d->bounds, uiBackground_ColorId);
250 drawRect_Paint(p, d->bounds, uiSeparator_ColorId); 253 drawRect_Paint(p, d->bounds, uiSeparator_ColorId);
251 iRect rect = d->bounds; 254 iRect rect = d->bounds;
@@ -275,3 +278,68 @@ void draw_DownloadUI(const iDownloadUI *d, iPaint *p) {
275 translateCStr_Lang("\u2014 ${mb.per.sec}")); 278 translateCStr_Lang("\u2014 ${mb.per.sec}"));
276 } 279 }
277} 280}
281
282/*----------------------------------------------------------------------------------------------*/
283
284void init_FontpackUI(iFontpackUI *d, const iMedia *media, uint16_t mediaId, iRect bounds) {
285 d->media = media;
286 d->mediaId = mediaId;
287 d->bounds = bounds;
288 d->installRect.size = add_I2(measure_Text(uiLabel_FontId, "${media.fontpack.install}").bounds.size,
289 muli_I2(gap2_UI, 3));
290 d->installRect.pos.x = right_Rect(d->bounds) - gap_UI - d->installRect.size.x;
291 d->installRect.pos.y = mid_Rect(d->bounds).y - d->installRect.size.y / 2;
292}
293
294iBool processEvent_FontpackUI(iFontpackUI *d, const SDL_Event *ev) {
295 switch (ev->type) {
296 case SDL_MOUSEBUTTONDOWN:
297 case SDL_MOUSEBUTTONUP: {
298 const iInt2 pos = init_I2(ev->button.x, ev->button.y);
299 if (contains_Rect(d->installRect, pos)) {
300 return iTrue;
301 }
302 break;
303 }
304 case SDL_MOUSEMOTION:
305 if (contains_Rect(d->bounds, init_I2(ev->motion.x, ev->motion.y))) {
306 return iTrue;
307 }
308 break;
309 }
310 return iFalse;
311}
312
313int height_FontpackUI(const iMedia *media, uint16_t mediaId, int width) {
314 const iStringList *names;
315 iFontpackMediaInfo info;
316 fontpackInfo_Media(media, (iMediaId){ fontpack_MediaType, mediaId }, &info);
317 return lineHeight_Text(uiContent_FontId) +
318 lineHeight_Text(uiLabel_FontId) * (1 + size_StringList(info.names));
319}
320
321void draw_FontpackUI(const iFontpackUI *d, iPaint *p) {
322 /* Draw a background box. */
323 fillRect_Paint(p, d->bounds, uiBackground_ColorId);
324 drawRect_Paint(p, d->bounds, uiSeparator_ColorId);
325 iFontpackMediaInfo info;
326 fontpackInfo_Media(d->media, (iMediaId){ fontpack_MediaType, d->mediaId }, &info);
327 iInt2 pos = topLeft_Rect(d->bounds);
328 const char *checks[] = { "\u2610", "\u2611" };
329 draw_Text(uiContentBold_FontId, pos, uiHeading_ColorId, "\"%s\" v%d",
330 cstr_String(info.packId.id), info.packId.version);
331 pos.y += lineHeight_Text(uiContentBold_FontId);
332 draw_Text(uiLabelBold_FontId, pos, uiText_ColorId, "%.1f MB, %d fonts %s %s %s",
333 info.sizeInBytes / 1.0e6, size_StringList(info.names),
334// checks[info.isValid], info.isValid ? "No errors" : "Errors detected",
335 checks[info.isInstalled], info.isInstalled ? "Installed" : "Not installed",
336 info.isReadOnly ? "Read-Only" : "");
337 pos.y += lineHeight_Text(uiLabelBold_FontId);
338 iConstForEach(StringList, i, info.names) {
339 drawRange_Text(uiLabel_FontId, pos, uiText_ColorId, range_String(i.value));
340 pos.y += lineHeight_Text(uiLabel_FontId);
341 }
342 /* Buttons. */
343 drawInlineButton_(p, d->installRect,
344 "${media.fontpack.install}", uiLabel_FontId);
345}
diff --git a/src/ui/mediaui.h b/src/ui/mediaui.h
index e79dedc0..73de1994 100644
--- a/src/ui/mediaui.h
+++ b/src/ui/mediaui.h
@@ -51,11 +51,27 @@ iDeclareType(Media)
51iDeclareType(DownloadUI) 51iDeclareType(DownloadUI)
52 52
53struct Impl_DownloadUI { 53struct Impl_DownloadUI {
54 const iDocumentWidget *doc; 54 const iMedia *media;
55 uint16_t mediaId; 55 uint16_t mediaId;
56 iRect bounds; 56 iRect bounds;
57}; 57};
58 58
59void init_DownloadUI (iDownloadUI *, const iDocumentWidget *doc, uint16_t mediaId, iRect bounds); 59void init_DownloadUI (iDownloadUI *, const iMedia *media, uint16_t mediaId, iRect bounds);
60iBool processEvent_DownloadUI (iDownloadUI *, const SDL_Event *ev); 60iBool processEvent_DownloadUI (iDownloadUI *, const SDL_Event *ev);
61void draw_DownloadUI (const iDownloadUI *, iPaint *p); 61void draw_DownloadUI (const iDownloadUI *, iPaint *p);
62
63/*----------------------------------------------------------------------------------------------*/
64
65iDeclareType(FontpackUI)
66
67struct Impl_FontpackUI {
68 const iMedia *media;
69 uint16_t mediaId;
70 iRect bounds;
71 iRect installRect;
72};
73
74void init_FontpackUI (iFontpackUI *, const iMedia *media, uint16_t mediaId, iRect bounds);
75int height_FontpackUI (const iMedia *media, uint16_t mediaId, int width);
76iBool processEvent_FontpackUI (iFontpackUI *, const SDL_Event *ev);
77void draw_FontpackUI (const iFontpackUI *, iPaint *p);