@effindomv2/fui-as 0.1.27 → 0.1.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,5 @@
1
1
  import type {
2
+ WorkerBootstrapFileProcessStartMessage,
2
3
  WorkerBootstrapInboundMessage,
3
4
  WorkerBootstrapOutboundMessage,
4
5
  WorkerHostServicesBundleConfig,
@@ -22,10 +23,13 @@ const encoder = new TextEncoder();
22
23
  let activeWorkerId: number | null = null;
23
24
  let activeCancellationRequested = false;
24
25
  let pendingCancellationRequested = false;
26
+ let activeFile: File | null = null;
25
27
 
26
28
  const allowedWorkerHostImports = new Set([
27
29
  'fui_fetch_start',
28
30
  'fui_fetch_cancel',
31
+ 'fui_file_read_chunk',
32
+ 'fui_file_worker_write_chunk',
29
33
  'fui_worker_input_length',
30
34
  'fui_worker_copy_input',
31
35
  'fui_worker_report_progress',
@@ -369,6 +373,45 @@ async function startWorker(message: WorkerBootstrapStartMessage): Promise<void>
369
373
  yieldRequested = true;
370
374
  requestedYieldDelayMs = Number.isFinite(delayMs) && delayMs > 0 ? Math.floor(delayMs) : 0;
371
375
  },
376
+ fui_file_read_chunk(offsetLow: number, offsetHigh: number, length: number): number {
377
+ if (activeFile === null) {
378
+ return 0;
379
+ }
380
+ const offset = Number(BigInt(offsetLow >>> 0) | (BigInt(offsetHigh >>> 0) << 32n));
381
+ const safeLength = Math.max(0, length | 0);
382
+ if (offset >= activeFile.size || safeLength <= 0) {
383
+ return 0;
384
+ }
385
+ const blob = activeFile.slice(offset, Math.min(offset + safeLength, activeFile.size));
386
+ const reader = new FileReaderSync();
387
+ const buffer = reader.readAsArrayBuffer(blob);
388
+ const bytes = new Uint8Array(buffer);
389
+ const written = bytes.length;
390
+ if (written <= 0) {
391
+ return 0;
392
+ }
393
+ if (memory === null || callbackBufferSize <= 0) {
394
+ return 0;
395
+ }
396
+ if (written > callbackBufferSize) {
397
+ throw new Error('File chunk exceeds the worker callback buffer.');
398
+ }
399
+ new Uint8Array(memory.buffer, callbackBufferPtr, written).set(bytes);
400
+ return written;
401
+ },
402
+ fui_file_worker_write_chunk(ptr: number, len: number): void {
403
+ if (memory === null || len <= 0) {
404
+ return;
405
+ }
406
+ const bytes = new Uint8Array(len);
407
+ bytes.set(new Uint8Array(memory.buffer, ptr, len));
408
+ const buffer = bytes.buffer.slice(0, bytes.byteLength) as ArrayBuffer;
409
+ workerScope.postMessage({
410
+ type: 'file-process-chunk',
411
+ workerId: message.workerId,
412
+ bytes: buffer,
413
+ }, [buffer]);
414
+ },
372
415
  },
373
416
  fui_fetch_host: {
374
417
  fui_fetch_start(
@@ -494,12 +537,375 @@ async function startWorker(message: WorkerBootstrapStartMessage): Promise<void>
494
537
  }
495
538
  }
496
539
 
540
+ async function startFileProcessWorker(message: WorkerBootstrapFileProcessStartMessage): Promise<void> {
541
+ let memory: WebAssembly.Memory | null = null;
542
+ let terminalSent = false;
543
+ let callbackBufferPtr = 0;
544
+ let callbackBufferSize = 0;
545
+ let wasmExports: (Record<string, unknown> & {
546
+ memory?: WebAssembly.Memory;
547
+ __fui_worker_text_buffer?: () => number;
548
+ __fui_worker_text_buffer_size?: () => number;
549
+ }) | null = null;
550
+ const activeFetchRequests = new Map<number, AbortController>();
551
+ let entry: (() => void) | null = null;
552
+ activeWorkerId = message.workerId;
553
+ activeCancellationRequested = pendingCancellationRequested;
554
+ activeFile = message.file;
555
+ pendingCancellationRequested = false;
556
+
557
+ function readCancelFlag(): boolean {
558
+ return activeWorkerId === message.workerId && activeCancellationRequested;
559
+ }
560
+
561
+ function cancelAllFetchRequests(): void {
562
+ for (const controller of activeFetchRequests.values()) {
563
+ controller.abort();
564
+ }
565
+ activeFetchRequests.clear();
566
+ }
567
+
568
+ function writeCallbackBytes(bytes: Uint8Array, context: string): { ptr: number; len: number } {
569
+ if (callbackBufferSize <= 0) {
570
+ throw new Error(`${context} requires the worker callback buffer.`);
571
+ }
572
+ if (bytes.length > callbackBufferSize) {
573
+ throw new Error(`${context} exceeds the worker callback buffer.`);
574
+ }
575
+ if (memory === null) {
576
+ throw new Error(`${context} requires worker memory.`);
577
+ }
578
+ if (bytes.length > 0) {
579
+ new Uint8Array(memory.buffer, callbackBufferPtr, bytes.length).set(bytes);
580
+ }
581
+ return {
582
+ ptr: bytes.length > 0 ? callbackBufferPtr : 0,
583
+ len: bytes.length,
584
+ };
585
+ }
586
+
587
+ function emitFetchComplete(
588
+ requestId: number,
589
+ ok: boolean,
590
+ status: number,
591
+ statusText: string,
592
+ url: string,
593
+ exports: Record<string, unknown>,
594
+ ): void {
595
+ const callback = exports.__fui_on_fetch_complete;
596
+ if (typeof callback !== 'function') {
597
+ throw new Error('Worker module is missing __fui_on_fetch_complete.');
598
+ }
599
+ const payload = writeCallbackBytes(encodeTextPartsPayload([statusText, url]), 'Worker fetch completion payload');
600
+ (callback as (requestId: number, ok: boolean, status: number, payloadPtr: number, payloadLen: number) => void)(
601
+ requestId,
602
+ ok,
603
+ status,
604
+ payload.ptr,
605
+ payload.len,
606
+ );
607
+ }
608
+
609
+ function emitFetchError(
610
+ requestId: number,
611
+ message: string,
612
+ exports: Record<string, unknown>,
613
+ ): void {
614
+ const callback = exports.__fui_on_fetch_error;
615
+ if (typeof callback !== 'function') {
616
+ throw new Error('Worker module is missing __fui_on_fetch_error.');
617
+ }
618
+ const payload = writeCallbackBytes(encoder.encode(message), 'Worker fetch failure payload');
619
+ (callback as (requestId: number, payloadPtr: number, payloadLen: number) => void)(
620
+ requestId,
621
+ payload.ptr,
622
+ payload.len,
623
+ );
624
+ }
625
+
626
+ function runEntry(): void {
627
+ if (entry === null || wasmExports === null) {
628
+ return;
629
+ }
630
+ try {
631
+ entry();
632
+ } catch (error: unknown) {
633
+ if (terminalSent) {
634
+ return;
635
+ }
636
+ terminalSent = true;
637
+ cancelAllFetchRequests();
638
+ activeWorkerId = null;
639
+ activeCancellationRequested = false;
640
+ activeFile = null;
641
+ workerScope.postMessage({
642
+ type: 'error',
643
+ workerId: message.workerId,
644
+ text: describeError(error),
645
+ });
646
+ return;
647
+ }
648
+ if (terminalSent) {
649
+ return;
650
+ }
651
+ }
652
+
653
+ function cleanupTerminal(): void {
654
+ terminalSent = true;
655
+ cancelAllFetchRequests();
656
+ activeWorkerId = null;
657
+ activeCancellationRequested = false;
658
+ }
659
+ try {
660
+ const response = await fetch(message.wasmUrl, {
661
+ cache: 'no-store',
662
+ credentials: 'same-origin',
663
+ });
664
+ if (!response.ok) {
665
+ throw new Error(`Failed to load worker wasm from ${message.wasmUrl}.`);
666
+ }
667
+ const bytes = await response.arrayBuffer();
668
+ const module = await WebAssembly.compile(bytes);
669
+ const hostServices = loadWorkerHostServices(message.workerHostServices);
670
+ validateWorkerImports(module, hostServices);
671
+ const instance = await WebAssembly.instantiate(module, {
672
+ env: {
673
+ abort(_message?: number, _fileName?: number, line?: number, column?: number): never {
674
+ throw new Error(`Worker aborted at ${String(line ?? 0)}:${String(column ?? 0)}.`);
675
+ },
676
+ },
677
+ fui_host_service: createHostServiceImportModule(hostServices, {
678
+ readString: (ptr, len) => readUtf8(memory, ptr, len),
679
+ writeString: (ptr, capacity, text, context) => writeUtf8(memory, ptr, capacity, text, context),
680
+ readBytes: (ptr, len) => readBytes(memory, ptr, len),
681
+ writeBytes: (ptr, capacity, bytes, context) => writeBytes(memory, ptr, capacity, bytes, context),
682
+ }),
683
+ fui_worker_host: {
684
+ fui_worker_input_length(): number {
685
+ return 0;
686
+ },
687
+ fui_worker_copy_input(_ptr: number, _capacity: number): number {
688
+ return 0;
689
+ },
690
+ fui_worker_report_progress(ptr: number, len: number): void {
691
+ if (terminalSent) {
692
+ return;
693
+ }
694
+ workerScope.postMessage({
695
+ type: 'progress',
696
+ workerId: message.workerId,
697
+ text: readUtf8(memory, ptr, len),
698
+ });
699
+ },
700
+ fui_worker_complete_string(ptr: number, len: number): void {
701
+ if (terminalSent) {
702
+ return;
703
+ }
704
+ cleanupTerminal();
705
+ activeFile = null;
706
+ workerScope.postMessage({
707
+ type: 'complete',
708
+ workerId: message.workerId,
709
+ text: readUtf8(memory, ptr, len),
710
+ });
711
+ },
712
+ fui_worker_fail(ptr: number, len: number): void {
713
+ if (terminalSent) {
714
+ return;
715
+ }
716
+ cleanupTerminal();
717
+ activeFile = null;
718
+ workerScope.postMessage({
719
+ type: 'error',
720
+ workerId: message.workerId,
721
+ text: readUtf8(memory, ptr, len),
722
+ });
723
+ },
724
+ fui_worker_is_cancelled(): number {
725
+ return readCancelFlag() ? 1 : 0;
726
+ },
727
+ fui_worker_request_yield(): void {
728
+ // File processing is synchronous — yield is a no-op.
729
+ },
730
+ fui_worker_request_yield_delay(_delayMs: number): void {
731
+ // File processing is synchronous — yield is a no-op.
732
+ },
733
+ fui_file_read_chunk(offsetLow: number, offsetHigh: number, length: number): number {
734
+ if (activeFile === null) {
735
+ return 0;
736
+ }
737
+ const offset = Number(BigInt(offsetLow >>> 0) | (BigInt(offsetHigh >>> 0) << 32n));
738
+ const safeLength = Math.max(0, length | 0);
739
+ if (offset >= activeFile.size || safeLength <= 0) {
740
+ return 0;
741
+ }
742
+ const blob = activeFile.slice(offset, Math.min(offset + safeLength, activeFile.size));
743
+ const reader = new FileReaderSync();
744
+ const buffer = reader.readAsArrayBuffer(blob);
745
+ const readBytes = new Uint8Array(buffer);
746
+ const written = readBytes.length;
747
+ if (written <= 0) {
748
+ return 0;
749
+ }
750
+ if (memory === null || callbackBufferSize <= 0) {
751
+ return 0;
752
+ }
753
+ if (written > callbackBufferSize) {
754
+ throw new Error('File chunk exceeds the worker callback buffer.');
755
+ }
756
+ new Uint8Array(memory.buffer, callbackBufferPtr, written).set(readBytes);
757
+ return written;
758
+ },
759
+ fui_file_worker_write_chunk(ptr: number, len: number): void {
760
+ if (memory === null || len <= 0) {
761
+ return;
762
+ }
763
+ const chunkBytes = new Uint8Array(len);
764
+ chunkBytes.set(new Uint8Array(memory.buffer, ptr, len));
765
+ const buffer = chunkBytes.buffer.slice(0, chunkBytes.byteLength) as ArrayBuffer;
766
+ workerScope.postMessage({
767
+ type: 'file-process-chunk',
768
+ workerId: message.workerId,
769
+ bytes: buffer,
770
+ }, [buffer]);
771
+ },
772
+ },
773
+ fui_fetch_host: {
774
+ fui_fetch_start(
775
+ requestId: number,
776
+ methodPtr: number,
777
+ methodLen: number,
778
+ urlPtr: number,
779
+ urlLen: number,
780
+ headersPtr: number,
781
+ headersLen: number,
782
+ bodyPtr: number,
783
+ bodyLen: number,
784
+ ): void {
785
+ const controller = new AbortController();
786
+ const method = readUtf8(memory, methodPtr, methodLen);
787
+ const url = readUtf8(memory, urlPtr, urlLen);
788
+ const headerBytes = memory === null || headersLen <= 0
789
+ ? new Uint8Array(0)
790
+ : new Uint8Array(memory.buffer.slice(headersPtr, headersPtr + headersLen));
791
+ if (headerBytes.byteLength < 4 && headersLen > 0) {
792
+ throw new Error('Worker fetch header payload was truncated.');
793
+ }
794
+ const headers = new Headers();
795
+ if (headerBytes.byteLength >= 4) {
796
+ const dataView = new DataView(headerBytes.buffer, headerBytes.byteOffset, headerBytes.byteLength);
797
+ let byteOffset = 0;
798
+ const count = dataView.getUint32(byteOffset, true);
799
+ byteOffset += 4;
800
+ const values: string[] = [];
801
+ for (let index = 0; index < count; index += 1) {
802
+ if (byteOffset + 4 > headerBytes.byteLength) {
803
+ throw new Error('Worker fetch header length was truncated.');
804
+ }
805
+ const partLen = dataView.getUint32(byteOffset, true);
806
+ byteOffset += 4;
807
+ if (byteOffset + partLen > headerBytes.byteLength) {
808
+ throw new Error('Worker fetch header value was truncated.');
809
+ }
810
+ values.push(partLen > 0 ? decoder.decode(headerBytes.subarray(byteOffset, byteOffset + partLen)) : '');
811
+ byteOffset += partLen;
812
+ }
813
+ if ((values.length & 1) != 0) {
814
+ throw new Error('Worker fetch headers were malformed.');
815
+ }
816
+ for (let index = 0; index < values.length; index += 2) {
817
+ headers.append(values[index] ?? '', values[index + 1] ?? '');
818
+ }
819
+ }
820
+ const bodyBytes = memory === null || bodyLen <= 0
821
+ ? new Uint8Array(0)
822
+ : new Uint8Array(memory.buffer, bodyPtr, bodyLen);
823
+ const body = bodyBytes.length > 0 ? bodyBytes : undefined;
824
+ activeFetchRequests.set(requestId, controller);
825
+ void fetch(url, {
826
+ method,
827
+ headers,
828
+ body,
829
+ signal: controller.signal,
830
+ }).then((response) => {
831
+ const active = activeFetchRequests.get(requestId);
832
+ if (active === undefined || active !== controller || terminalSent) {
833
+ return;
834
+ }
835
+ activeFetchRequests.delete(requestId);
836
+ if (wasmExports === null) {
837
+ throw new Error('Worker fetch completed before wasm exports were ready.');
838
+ }
839
+ emitFetchComplete(requestId, response.ok, response.status, response.statusText, response.url, wasmExports);
840
+ }).catch((error: unknown) => {
841
+ const active = activeFetchRequests.get(requestId);
842
+ if (active === undefined || active !== controller) {
843
+ return;
844
+ }
845
+ activeFetchRequests.delete(requestId);
846
+ if (controller.signal.aborted || terminalSent) {
847
+ return;
848
+ }
849
+ if (wasmExports === null) {
850
+ throw new Error('Worker fetch failed before wasm exports were ready.');
851
+ }
852
+ emitFetchError(requestId, describeError(error), wasmExports);
853
+ });
854
+ },
855
+ fui_fetch_cancel(requestId: number): void {
856
+ const controller = activeFetchRequests.get(requestId);
857
+ if (controller === undefined) {
858
+ return;
859
+ }
860
+ activeFetchRequests.delete(requestId);
861
+ controller.abort();
862
+ },
863
+ },
864
+ });
865
+ const exports = instance.exports as Record<string, unknown> & {
866
+ memory?: WebAssembly.Memory;
867
+ __fui_worker_text_buffer?: () => number;
868
+ __fui_worker_text_buffer_size?: () => number;
869
+ };
870
+ wasmExports = exports;
871
+ if (!(exports.memory instanceof WebAssembly.Memory)) {
872
+ throw new Error('Worker module did not export memory.');
873
+ }
874
+ memory = exports.memory;
875
+ if (typeof exports.__fui_worker_text_buffer !== 'function' || typeof exports.__fui_worker_text_buffer_size !== 'function') {
876
+ throw new Error('Worker module did not export the fetch callback buffer.');
877
+ }
878
+ callbackBufferPtr = exports.__fui_worker_text_buffer();
879
+ callbackBufferSize = exports.__fui_worker_text_buffer_size();
880
+ const exportedEntry = exports[message.entryName];
881
+ if (typeof exportedEntry !== 'function') {
882
+ throw new Error(`Worker export "${message.entryName}" is missing.`);
883
+ }
884
+ entry = exportedEntry as () => void;
885
+ runEntry();
886
+ } catch (error: unknown) {
887
+ cancelAllFetchRequests();
888
+ activeWorkerId = null;
889
+ activeCancellationRequested = false;
890
+ activeFile = null;
891
+ workerScope.postMessage({
892
+ type: 'error',
893
+ workerId: message.workerId,
894
+ text: describeError(error),
895
+ });
896
+ }
897
+ }
898
+
497
899
  workerScope.onmessage = (event: MessageEvent<WorkerBootstrapInboundMessage>) => {
498
900
  const message = event.data;
499
901
  if (message.type === 'start') {
500
902
  void startWorker(message);
501
903
  return;
502
904
  }
905
+ if (message.type === 'start-file-process') {
906
+ void startFileProcessWorker(message);
907
+ return;
908
+ }
503
909
  if (message.type === 'cancel') {
504
910
  if (activeWorkerId === message.workerId) {
505
911
  activeCancellationRequested = true;
@@ -121,6 +121,10 @@ export function createWorkerManager(options: WorkerManagerOptions): WorkerManage
121
121
  finishWorker(workerId);
122
122
  return;
123
123
  }
124
+ // File-process chunk messages are routed through the file host, not the worker manager.
125
+ if (message.type === 'file-process-chunk') {
126
+ return;
127
+ }
124
128
  emitToSession(workerId, 'error', message.text);
125
129
  finishWorker(workerId);
126
130
  }
@@ -17,6 +17,16 @@ export interface WorkerBootstrapStartMessage {
17
17
  readonly workerHostServices?: WorkerHostServicesBundleConfig;
18
18
  }
19
19
 
20
+ export interface WorkerBootstrapFileProcessStartMessage {
21
+ readonly type: "start-file-process";
22
+ readonly workerId: number;
23
+ readonly file: File;
24
+ readonly wasmUrl: string;
25
+ readonly entryName: string;
26
+ readonly chunkSize: number;
27
+ readonly workerHostServices?: WorkerHostServicesBundleConfig;
28
+ }
29
+
20
30
  export interface WorkerBootstrapCancelMessage {
21
31
  readonly type: "cancel";
22
32
  readonly workerId: number;
@@ -24,6 +34,7 @@ export interface WorkerBootstrapCancelMessage {
24
34
 
25
35
  export type WorkerBootstrapInboundMessage =
26
36
  | WorkerBootstrapStartMessage
37
+ | WorkerBootstrapFileProcessStartMessage
27
38
  | WorkerBootstrapCancelMessage;
28
39
 
29
40
  export interface WorkerBootstrapProgressMessage {
@@ -38,6 +49,12 @@ export interface WorkerBootstrapCompleteMessage {
38
49
  readonly text: string;
39
50
  }
40
51
 
52
+ export interface WorkerBootstrapFileProcessChunkMessage {
53
+ readonly type: "file-process-chunk";
54
+ readonly workerId: number;
55
+ readonly bytes: ArrayBuffer;
56
+ }
57
+
41
58
  export interface WorkerBootstrapErrorMessage {
42
59
  readonly type: "error";
43
60
  readonly workerId: number;
@@ -47,4 +64,5 @@ export interface WorkerBootstrapErrorMessage {
47
64
  export type WorkerBootstrapOutboundMessage =
48
65
  | WorkerBootstrapProgressMessage
49
66
  | WorkerBootstrapCompleteMessage
67
+ | WorkerBootstrapFileProcessChunkMessage
50
68
  | WorkerBootstrapErrorMessage;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effindomv2/fui-as",
3
- "version": "0.1.27",
3
+ "version": "0.1.30",
4
4
  "private": false,
5
5
  "license": "AGPL-3.0-only OR LicenseRef-EffinDom-Commercial",
6
6
  "description": "EffinDom v2 AssemblyScript frontend framework SDK and browser harness",
@@ -86,7 +86,8 @@
86
86
  },
87
87
  "dependencies": {
88
88
  "@assemblyscript/loader": "^0.28.17",
89
- "@effindomv2/runtime": "0.1.10"
89
+ "@devcycle/assemblyscript-json": "^2.0.0",
90
+ "@effindomv2/runtime": "0.1.13"
90
91
  },
91
92
  "devDependencies": {
92
93
  "@as-pect/assembly": "8.1.0",
package/src/Fui.ts CHANGED
@@ -138,6 +138,7 @@ export { frameTimeSignal, viewportHeightSignal, viewportWidthSignal } from "./co
138
138
  export {
139
139
  AntiSelectionArea,
140
140
  Button,
141
+ ButtonColors,
141
142
  ButtonPresenter,
142
143
  ButtonTemplate,
143
144
  ButtonVisualState,
package/src/FuiWorker.ts CHANGED
@@ -1,3 +1,7 @@
1
1
  export { Fetch, FetchRequest, FetchResponse } from "./core/Fetch";
2
- export { Worker as WorkerRuntime } from "./worker/Worker";
2
+ export { Worker, Worker as WorkerRuntime } from "./worker/Worker";
3
3
  export { WorkerJob } from "./worker/WorkerJob";
4
+ export {
5
+ fui_file_read_chunk,
6
+ fui_file_worker_write_chunk,
7
+ } from "./worker/ffi";
@@ -20,6 +20,7 @@ import { Signal } from "../core/Signal";
20
20
  import { Theme, activeTheme } from "../core/Theme";
21
21
  import { FontFamily, FontStyle, FontWeight } from "../core/Typography";
22
22
  import { FlexBox, TextCore } from "../nodes";
23
+ import { ButtonColors } from "./ButtonColors";
23
24
  import { getControlTemplates } from "./ControlTemplateSet";
24
25
  import {
25
26
  ButtonPresenter,
@@ -119,6 +120,7 @@ export class Button extends FlexBox {
119
120
  private shadowOffsetYValue: f32 = 0.0;
120
121
  private shadowBlurValue: f32 = 0.0;
121
122
  private shadowSpreadValue: f32 = 0.0;
123
+ private colorsValue: ButtonColors | null = null;
122
124
  private templateValue: ButtonTemplate | null = null;
123
125
  private presenterNeedsRefresh: bool = false;
124
126
 
@@ -203,6 +205,12 @@ export class Button extends FlexBox {
203
205
  return this;
204
206
  }
205
207
 
208
+ colors(colors: ButtonColors | null): this {
209
+ this.colorsValue = colors;
210
+ this.handleThemeSignalChanged();
211
+ return this;
212
+ }
213
+
206
214
  bgColor(color: u32): this {
207
215
  this.backgroundOverridden = true;
208
216
  this.normalBackgroundColorValue = color;
@@ -499,14 +507,31 @@ export class Button extends FlexBox {
499
507
  }
500
508
 
501
509
  private syncThemeState(theme: Theme): void {
510
+ const colors = this.colorsValue;
502
511
  if (!this.backgroundOverridden) {
503
- this.normalBackgroundColorValue = theme.colors.accent;
512
+ this.normalBackgroundColorValue = colors !== null && colors.hasBackground
513
+ ? colors.backgroundColor
514
+ : theme.colors.accent;
504
515
  }
505
516
  if (!this.hoverBackgroundOverridden) {
506
- this.hoverBackgroundColorValue = theme.colors.accentHovered;
517
+ if (colors !== null && colors.hasBackgroundHover) {
518
+ this.hoverBackgroundColorValue = colors.backgroundHoverColor;
519
+ } else if (colors !== null && colors.hasBackground) {
520
+ this.hoverBackgroundColorValue = colors.backgroundColor;
521
+ } else {
522
+ this.hoverBackgroundColorValue = theme.colors.accentHovered;
523
+ }
507
524
  }
508
525
  if (!this.pressedBackgroundOverridden) {
509
- this.pressedBackgroundColorValue = theme.colors.accentPressed;
526
+ if (colors !== null && colors.hasBackgroundPressed) {
527
+ this.pressedBackgroundColorValue = colors.backgroundPressedColor;
528
+ } else if (colors !== null && colors.hasBackgroundHover) {
529
+ this.pressedBackgroundColorValue = colors.backgroundHoverColor;
530
+ } else if (colors !== null && colors.hasBackground) {
531
+ this.pressedBackgroundColorValue = colors.backgroundColor;
532
+ } else {
533
+ this.pressedBackgroundColorValue = theme.colors.accentPressed;
534
+ }
510
535
  }
511
536
  if (!this.cornerRadiusOverridden) {
512
537
  this.focusCornerTopLeft = theme.spacing.sm;
@@ -516,7 +541,9 @@ export class Button extends FlexBox {
516
541
  }
517
542
  if (!this.borderOverridden) {
518
543
  this.borderWidthValue = 1.0;
519
- this.borderColorValue = theme.colors.border;
544
+ this.borderColorValue = colors !== null && colors.hasBorder
545
+ ? colors.borderColor
546
+ : theme.colors.border;
520
547
  this.borderStyleValue = BorderStyle.Solid;
521
548
  this.borderDashedValue = false;
522
549
  }
@@ -543,7 +570,13 @@ export class Button extends FlexBox {
543
570
  this.fontSizeValue = theme.fonts.sizeBody;
544
571
  }
545
572
  if (!this.textColorOverridden) {
546
- this.textColorValue = theme.colors.textOnAccent;
573
+ if (!this.isEnabled && colors !== null && colors.hasTextMuted) {
574
+ this.textColorValue = colors.textMutedColor;
575
+ } else if (colors !== null && colors.hasTextPrimary) {
576
+ this.textColorValue = colors.textPrimaryColor;
577
+ } else {
578
+ this.textColorValue = theme.colors.textOnAccent;
579
+ }
547
580
  }
548
581
  const presenterHostState = new ButtonPresenterHostState(
549
582
  this.backgroundOverridden,
@@ -572,7 +605,7 @@ export class Button extends FlexBox {
572
605
  this.paddingRightValue,
573
606
  this.paddingBottomValue,
574
607
  );
575
- this.presenter.apply(theme, this.createVisualState());
608
+ this.presenter.apply(theme, this.createVisualState(), this.colorsValue);
576
609
  this.backgroundOverridden = presenterHostState.backgroundOverridden;
577
610
  this.normalBackgroundColorValue = presenterHostState.normalBackgroundColorValue;
578
611
  this.cornerRadiusOverridden = presenterHostState.cornerRadiusOverridden;