k-l-lambda commited on
Commit
9659b5a
·
1 Parent(s): 256d374

refined UI.

Browse files
Files changed (2) hide show
  1. app.py +114 -11
  2. web/layout-fit.js +65 -0
app.py CHANGED
@@ -350,6 +350,9 @@ def build_head ():
350
  # the mount/bridge script that wires it to the hidden #ls-editor-state textbox.
351
  os.path.join(vendor, 'lyl-editor.bundle.js'),
352
  os.path.join(WEB_DIR, 'lyl-editor-mount.js'),
 
 
 
353
  ]
354
  tags = ['<script>window.__LILYSCRIPT_SOUNDFONT_URL=%r;window.__LILYSCRIPT_FLUID_URL=%r;</script>'
355
  % (_file_url(os.path.join(WEB_DIR, 'soundfont')) + '/',
@@ -439,10 +442,9 @@ CUSTOM_CSS = '''
439
  white-space: normal;
440
  word-break: break-all;
441
  }
442
- /* Score List: fixed height with an auto scrollbar, matching the editor's
443
- 18-line viewport (gr.Code lines=18 266px). */
444
  .score-list {
445
- max-height: 324px;
446
  overflow-y: auto;
447
  }
448
  /* Generate button turns yellow while a generation is running (the .ls-generating
@@ -464,6 +466,107 @@ CUSTOM_CSS = '''
464
  #stop-btn.ls-generating:hover {
465
  background: #cf2e2e !important;
466
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
467
  '''
468
 
469
 
@@ -474,11 +577,11 @@ def build_ui ():
474
  gr.Markdown('## 🎼 LilyScript — symbolic music generation with Lilylet')
475
  store = gr.State(examples)
476
 
477
- with gr.Row(equal_height=True):
478
  # ---------------- LEFT ----------------
479
- with gr.Column(scale=5):
480
  # (1) compose params, with (2) the collapsible run log stacked below
481
- with gr.Group():
482
  gr.Markdown('## Compose')
483
  with gr.Group():
484
  gr.Markdown('- Style Options')
@@ -503,19 +606,19 @@ def build_ui ():
503
  gen_btn = gr.Button('Generate', variant='primary', elem_id='gen-btn')
504
  stop_btn = gr.Button('Stop', variant='stop', elem_id='stop-btn')
505
 
506
- with gr.Accordion('Logs', open=True):
507
  log = gr.Textbox(show_label=False, lines=10, max_lines=10,
508
  autoscroll=True, interactive=False, container=False)
509
 
510
- # bottom row: (3) file list | (4) editor
511
- with gr.Row(equal_height=True):
512
  with gr.Column(scale=2, min_width=160):
513
  with gr.Group():
514
  gr.Markdown('## Score List')
515
  file_list = gr.Radio(show_label=False, choices=list(examples.keys()),
516
  value=None, interactive=True, container=False,
517
  elem_classes=['score-list'])
518
- with gr.Column(scale=5):
519
  with gr.Group():
520
  gr.Markdown('## Lilylet editor')
521
  # Our own CodeMirror 6 editor (lyl-editor.bundle.js) mounts into
@@ -532,7 +635,7 @@ def build_ui ():
532
  elem_classes=['ls-editor-state-hidden'], show_label=False, container=False)
533
 
534
  # ---------------- RIGHT ----------------
535
- with gr.Column(scale=6):
536
  with gr.Group():
537
  gr.Markdown('## Sheet music')
538
  gr.HTML(SHEET_PLACEHOLDER)
 
350
  # the mount/bridge script that wires it to the hidden #ls-editor-state textbox.
351
  os.path.join(vendor, 'lyl-editor.bundle.js'),
352
  os.path.join(WEB_DIR, 'lyl-editor-mount.js'),
353
+ # computes --ls-fill-h so the bottom Score List | editor row fills the height
354
+ # left under Compose + Logs (robust against Gradio's flex-nesting quirks).
355
+ os.path.join(WEB_DIR, 'layout-fit.js'),
356
  ]
357
  tags = ['<script>window.__LILYSCRIPT_SOUNDFONT_URL=%r;window.__LILYSCRIPT_FLUID_URL=%r;</script>'
358
  % (_file_url(os.path.join(WEB_DIR, 'soundfont')) + '/',
 
442
  white-space: normal;
443
  word-break: break-all;
444
  }
445
+ /* Score List scrolls within the height its column is given (see the layout model
446
+ at the bottom of this stylesheet). */
447
  .score-list {
 
448
  overflow-y: auto;
449
  }
450
  /* Generate button turns yellow while a generation is running (the .ls-generating
 
466
  #stop-btn.ls-generating:hover {
467
  background: #cf2e2e !important;
468
  }
469
+
470
+ /* ---- Layout: viewport-locked two-column workspace ----------------------------
471
+ Goal: Compose + Logs keep their natural height; the Score List | editor row fills
472
+ the rest; a long score must NOT stretch the page (it scrolls inside the sheet
473
+ panel instead). Gradio's deep wrapper nesting makes pure-CSS flex height
474
+ propagation unreliable, so the panel heights are driven by a small ResizeObserver
475
+ in web/layout-fit.js (sets --ls-fill-h on :root = viewport − the panels above the
476
+ fill row). These rules consume that variable. */
477
+ #main-row {
478
+ align-items: flex-start; /* don't stretch columns to each other's height */
479
+ }
480
+ /* Right (Sheet music) column: the score preview scrolls inside a viewport-capped
481
+ height instead of growing the row. */
482
+ #sheet-col {
483
+ position: sticky;
484
+ top: 8px;
485
+ }
486
+ #sheet-col .ls-score-root {
487
+ height: calc(100vh - 110px);
488
+ min-height: 320px;
489
+ }
490
+ #sheet-col .ls-preview {
491
+ overflow: auto; /* the SVG scrolls here, not the page */
492
+ }
493
+
494
+ /* Left column stacks naturally: Compose, Logs, then the fill row. */
495
+ #compose-col {
496
+ display: flex;
497
+ flex-direction: column;
498
+ }
499
+ /* The bottom Score List | editor row gets the height left under Compose + Logs,
500
+ computed by layout-fit.js into --ls-fill-h (with a sane fallback + floor). */
501
+ #compose-col > .lp-fill {
502
+ height: var(--ls-fill-h, 420px);
503
+ min-height: 360px;
504
+ }
505
+ #compose-col > .lp-fill > .column,
506
+ #compose-col > .lp-fill > .column > .gr-group,
507
+ #compose-col > .lp-fill > .column > .gr-group > .gr-group {
508
+ height: 100%;
509
+ min-height: 0;
510
+ display: flex;
511
+ flex-direction: column;
512
+ }
513
+ /* Score-list column: the "## Score List" header (a .prose block) stays natural at
514
+ the top; the radio list scrolls in the remaining space. The header block sits in
515
+ the inner group alongside the radio, so pin it 0-shrink and let the list fill. */
516
+ #compose-col > .lp-fill > .column:first-child .block:has(.prose) {
517
+ flex: 0 0 auto;
518
+ }
519
+ #compose-col > .lp-fill > .column:first-child .block:has(.score-list) {
520
+ flex: 1 1 0;
521
+ min-height: 0;
522
+ display: flex;
523
+ flex-direction: column;
524
+ overflow: hidden;
525
+ }
526
+ #compose-col > .lp-fill > .column:first-child .score-list {
527
+ flex: 1 1 auto;
528
+ min-height: 0;
529
+ }
530
+ /* Logs textbox stays compact even on long output. */
531
+ #compose-col > .lp-fixed.gr-accordion textarea {
532
+ max-height: 200px;
533
+ }
534
+ /* Editor column: the embedded CodeMirror mount fills the space under its header.
535
+ DOM (Gradio): #editor-col > .gr-group > .gr-group > .styler > { headerBlock,
536
+ editorBlock(.html-container > .gradio-style > #ls-editor-mount), hiddenStateBlock }.
537
+ We make the chain down to .styler full-height flex columns, let the editor block
538
+ fill (flex:1, basis 0 so a long score can't inflate it), and keep the header
539
+ block natural. CM then scrolls inside the bounded mount. */
540
+ #editor-col,
541
+ #editor-col > .gr-group,
542
+ #editor-col > .gr-group > .gr-group,
543
+ #editor-col .styler {
544
+ display: flex;
545
+ flex-direction: column;
546
+ height: 100%;
547
+ min-height: 0;
548
+ }
549
+ /* the editor block (the one wrapping the gr.HTML mount) fills the styler height */
550
+ #editor-col .styler > .block:has(.html-container) {
551
+ flex: 1 1 0;
552
+ min-height: 0;
553
+ display: flex;
554
+ flex-direction: column;
555
+ overflow: hidden;
556
+ }
557
+ #editor-col .html-container,
558
+ #editor-col .html-container > .gradio-style {
559
+ flex: 1 1 0;
560
+ min-height: 0;
561
+ display: flex;
562
+ flex-direction: column;
563
+ overflow: hidden;
564
+ }
565
+ #editor-col #ls-editor-mount {
566
+ flex: 1 1 0;
567
+ height: auto; /* override lyl-editor.css fixed 470px */
568
+ min-height: 0;
569
+ }
570
  '''
571
 
572
 
 
577
  gr.Markdown('## 🎼 LilyScript — symbolic music generation with Lilylet')
578
  store = gr.State(examples)
579
 
580
+ with gr.Row(elem_id='main-row'):
581
  # ---------------- LEFT ----------------
582
+ with gr.Column(scale=5, elem_id='compose-col'):
583
  # (1) compose params, with (2) the collapsible run log stacked below
584
+ with gr.Group(elem_classes=['lp-fixed']):
585
  gr.Markdown('## Compose')
586
  with gr.Group():
587
  gr.Markdown('- Style Options')
 
606
  gen_btn = gr.Button('Generate', variant='primary', elem_id='gen-btn')
607
  stop_btn = gr.Button('Stop', variant='stop', elem_id='stop-btn')
608
 
609
+ with gr.Accordion('Logs', open=True, elem_classes=['lp-fixed']):
610
  log = gr.Textbox(show_label=False, lines=10, max_lines=10,
611
  autoscroll=True, interactive=False, container=False)
612
 
613
+ # bottom row: (3) file list | (4) editor — flex-fills the remaining height
614
+ with gr.Row(equal_height=True, elem_classes=['lp-fill']):
615
  with gr.Column(scale=2, min_width=160):
616
  with gr.Group():
617
  gr.Markdown('## Score List')
618
  file_list = gr.Radio(show_label=False, choices=list(examples.keys()),
619
  value=None, interactive=True, container=False,
620
  elem_classes=['score-list'])
621
+ with gr.Column(scale=5, elem_id='editor-col'):
622
  with gr.Group():
623
  gr.Markdown('## Lilylet editor')
624
  # Our own CodeMirror 6 editor (lyl-editor.bundle.js) mounts into
 
635
  elem_classes=['ls-editor-state-hidden'], show_label=False, container=False)
636
 
637
  # ---------------- RIGHT ----------------
638
+ with gr.Column(scale=6, elem_id='sheet-col'):
639
  with gr.Group():
640
  gr.Markdown('## Sheet music')
641
  gr.HTML(SHEET_PLACEHOLDER)
web/layout-fit.js ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* LilyScript layout fitter.
2
+ *
3
+ * Gradio's deep wrapper nesting makes pure-CSS flex height propagation unreliable
4
+ * (a long score in the right panel, or the editor's own content, can blow out the
5
+ * left column). So we compute the height available to the bottom "Score List |
6
+ * editor" row in JS and expose it as the CSS variable --ls-fill-h on :root.
7
+ *
8
+ * --ls-fill-h = viewport_height - fillRow.top - bottom_gap
9
+ *
10
+ * i.e. the fill row gets exactly the space left under Compose + Logs, down to the
11
+ * bottom of the viewport. score-player.css / app.py consume the variable. We
12
+ * recompute on resize and whenever the left column's size changes (Logs expanding,
13
+ * accordion toggle, fonts loading) via a ResizeObserver.
14
+ */
15
+ (function () {
16
+ 'use strict';
17
+
18
+ var BOTTOM_GAP = 16; // breathing room below the fill row
19
+ var MIN_FILL = 360; // never collapse the editor/score-list below this
20
+
21
+ function fillEl () {
22
+ var col = document.getElementById('compose-col');
23
+ return col ? col.querySelector(':scope > .lp-fill') : null;
24
+ }
25
+
26
+ function recompute () {
27
+ var fill = fillEl();
28
+ if (!fill) return;
29
+ var top = fill.getBoundingClientRect().top; // viewport-relative
30
+ var avail = window.innerHeight - top - BOTTOM_GAP;
31
+ if (avail < MIN_FILL) avail = MIN_FILL;
32
+ document.documentElement.style.setProperty('--ls-fill-h', avail + 'px');
33
+ }
34
+
35
+ var raf = null;
36
+ function schedule () {
37
+ if (raf) return;
38
+ raf = requestAnimationFrame(function () { raf = null; recompute(); });
39
+ }
40
+
41
+ function boot () {
42
+ recompute();
43
+ window.addEventListener('resize', schedule);
44
+ // observe the left column so Logs growth / accordion toggles re-fit
45
+ var col = document.getElementById('compose-col');
46
+ if (col && window.ResizeObserver) {
47
+ var ro = new ResizeObserver(schedule);
48
+ ro.observe(col);
49
+ // also observe the two fixed blocks directly (their height drives fill.top)
50
+ var fixed = col.querySelectorAll(':scope > .lp-fixed');
51
+ for (var i = 0; i < fixed.length; i++) ro.observe(fixed[i]);
52
+ }
53
+ // Gradio mounts asynchronously; retry a few times until #compose-col exists.
54
+ if (!col) setTimeout(boot, 300);
55
+ }
56
+
57
+ if (document.readyState === 'loading') {
58
+ document.addEventListener('DOMContentLoaded', boot);
59
+ } else {
60
+ boot();
61
+ }
62
+ // a late pass after fonts/score player settle
63
+ setTimeout(recompute, 1200);
64
+ console.log('[layout-fit] loaded');
65
+ })();