diff --git a/cell/api.js b/cell/api.js index 86f9bf72f1..3129f49385 100644 --- a/cell/api.js +++ b/cell/api.js @@ -3411,6 +3411,12 @@ var editor; this.wb = new AscCommonExcel.WorkbookView(this.wbModel, this.controller, this.handlers, this.HtmlElement, this.topLineEditorElement, this, this.collaborativeEditing, this.fontRenderingMode); + // Needed here for its scrollbar color sync, stringRender reset, and initial draw. + // Its wb.updateSkin() call recomputes the worksheet style the constructor above + // already set via updateDarkMode, a harmless one-time duplication rather than + // something this call is relied on to fix. + this.updateSkin(); + this.registerCustomFunctionsLibrary(undefined, true); if (this.isCopyOutEnabled && this.topLineEditorElement) { @@ -8628,6 +8634,9 @@ var editor; if (this.wb) { this.wb.updateSkin(); + if (this.wb.stringRender) { + this.wb.stringRender._reset(); + } var ws = this.wb.getWorksheet(); if (ws) { this.controller.updateScrollSettings(); @@ -8636,6 +8645,20 @@ var editor; } }; + spreadsheet_api.prototype.updateDarkMode = function () { + if (this.wb) { + this.wb.updateDarkMode(this.isDarkMode); + var ws = this.wb.getWorksheet(); + if (ws) { + ws.draw(); + } + // TODO: a cell actively being edited doesn't refresh to the new theme until editing + // ends (pre-existing limitation, not specific to dark mode). Fixing it live needs + // WorksheetView to re-derive the edited cell's own fill; we're accepting this small, + // self-correcting gap for now rather than adding that for a narrow, transient case. + } + }; + spreadsheet_api.prototype.turnOffSpecialModes = function() { let bResult = false; if (this.isStartAddShape) { diff --git a/cell/graphics/DrawingContext.js b/cell/graphics/DrawingContext.js index cc9f075e43..15c1a78653 100644 --- a/cell/graphics/DrawingContext.js +++ b/cell/graphics/DrawingContext.js @@ -446,9 +446,54 @@ // AscCommon.CColor this.fillColor = new AscCommon.CColor(255, 255, 255); + + ////// + // DarkMode support (DM) + this.isDarkMode = false; + + // DM / performance - cache for darkModeCorrectColor2 results, avoids recalculating the + // same color thousands of times per redraw. Never exposed directly, only read back via + // _darkModeColorShuttle. + this._darkModeRgbCache = {}; + + // DM / performance - one shared CColor reused for every getDarkModeCorrectedColor call, + // instead of allocating a new one each time. Safe because every caller reads it + // synchronously (setStrokeStyle/setFillStyle unpack it immediately) and never retains it. + this._darkModeColorShuttle = new AscCommon.CColor(0, 0, 0, 1); + return this; } + /** + * Returns the corrected color for the given automatic color. + * Callers should decide whether a color is eligible (explicit vs. automatic) before calling this. + * isDarkMode is not re-checked here: every current caller already gates the call itself on it. + * @param {Number} r 0-255 + * @param {Number} g 0-255 + * @param {Number} b 0-255 + * @param {Number} [a] 0-1 + * @return {AscCommon.CColor} the shared shuttle instance - read it immediately, it is + * overwritten by the next call, never store or mutate the reference + */ + DrawingContext.prototype.getDarkModeCorrectedColor = function (r, g, b, a) { + + var shuttle = this._darkModeColorShuttle; + var key = r + ',' + g + ',' + b; + var corrected = this._darkModeRgbCache[key]; + + if (!corrected) { + corrected = AscCommon.darkModeCorrectColor2(r, g, b); + this._darkModeRgbCache[key] = corrected; + } + + shuttle.put_r(corrected.R); + shuttle.put_g(corrected.G); + shuttle.put_b(corrected.B); + shuttle.a = a; + + return shuttle; + }; + DrawingContext.prototype._ppiInit = function () { this.scaleFactor = 1; diff --git a/cell/model/WorkbookElems.js b/cell/model/WorkbookElems.js index ca747b4de6..6f6a5d5a6b 100644 --- a/cell/model/WorkbookElems.js +++ b/cell/model/WorkbookElems.js @@ -139,6 +139,8 @@ var g_oRgbColorProperties = { function RgbColor(rgb) { this.rgb = rgb; + // true only for g_oDefaultFormat.ColorAuto, the "no color set" default + this.isAutoColor = false; this._hash; } @@ -153,7 +155,9 @@ RgbColor.prototype = }, clone : function() { - return new RgbColor(this.rgb); + var oColor = new RgbColor(this.rgb); + oColor.isAutoColor = this.isAutoColor; // stored on the color itself, survives cloning: true = still default, false = explicit (e.g. font color set by user/template) + return oColor; }, getType : function() { @@ -473,6 +477,28 @@ g_oColorManager = new ColorManager(); xfs: new CellXfs() }; + // Marks this instance as the "no explicit color set" default, as opposed to a user + // explicitly choosing literal black (a different RgbColor(0) instance). Identity alone + // doesn't survive .clone(), so callers like dark-mode text-color correction check this + // flag instead. + g_oDefaultFormat.ColorAuto.isAutoColor = true; + + // Is this color still Automatic, as opposed to something a user or template picked? + // Works for any color (font, border, ...): color.isAutoColor covers ColorAuto and its + // clones, and the identity check covers the one extra case that applies to font color + // once a workbook loads: g_oDefaultFormat.Font.c is then populated with the theme's + // default-text ThemeColor (see getBinaryOtherTableGVar in Serialize.js), and + // ThemeColor.clone() returns `this`, so that identity survives charProperties cloning. + function isColorAutomatic(color) { + if (!color) { + return true; + } + if (color.isAutoColor) { + return true; + } + return !!(g_oDefaultFormat.Font) && color === g_oDefaultFormat.Font.c; + } + window['AscCommonExcel'].isColorAutomatic = isColorAutomatic; /** @constructor */ function Fragment(val) { diff --git a/cell/utils/utils.js b/cell/utils/utils.js index c801c40af3..fe83f0459a 100644 --- a/cell/utils/utils.js +++ b/cell/utils/utils.js @@ -2538,13 +2538,18 @@ } } - function drawFillCell(ctx, graphics, fill, rect) { + function drawFillCell(ctx, graphics, fill, rect, bKeepsFillColorAsIs) { if (!fill.hasFill()) { return; } var solid = fill.getSolidFill(); if (solid) { + if (ctx.isDarkMode) { + if (!bKeepsFillColorAsIs) { + solid = ctx.getDarkModeCorrectedColor(solid.getR(), solid.getG(), solid.getB(), solid.getA()); + } + } ctx.setFillStyle(solid).fillRect(rect._x, rect._y, rect._width, rect._height); return; } diff --git a/cell/view/CellEditorView.js b/cell/view/CellEditorView.js index 8fc6cd7487..7f71aac97b 100644 --- a/cell/view/CellEditorView.js +++ b/cell/view/CellEditorView.js @@ -1633,7 +1633,9 @@ function (window, undefined) { } if (opt.fragments && opt.fragments.length > 0) { - t.textRender.render(undefined, t._getContentLeft(), dy || 0, t._getContentWidth(), opt.font.getColor()); + // keepsAutomaticTextColorAsIs reaches beginFragment's and handleBidiFlow's own + // lighting mode correction checks + t.textRender.render(undefined, t._getContentLeft(), dy || 0, t._getContentWidth(), opt.font.getColor(), opt.keepsAutomaticTextColorAsIs); } }; diff --git a/cell/view/StringRender.js b/cell/view/StringRender.js index c105a49a6d..7a89c75348 100644 --- a/cell/view/StringRender.js +++ b/cell/view/StringRender.js @@ -594,10 +594,13 @@ * @param {Number} y Top of the text rect * @param {Number} maxWidth Text width restriction * @param {String} textColor Default text color for formatless string + * @param {boolean} [bKeepsAutomaticTextColorAsIs] true when the effective background under + * this text (its own fill, the fixed search-highlight color, or nothing at all) is light + * enough that default/automatic text doesn't need dark-mode color correction * @return {StringRender} Returns 'this' to allow chaining */ - StringRender.prototype.render = function (drawingCtx, x, y, maxWidth, textColor) { - this._doRender(drawingCtx, x, y, maxWidth, textColor); + StringRender.prototype.render = function (drawingCtx, x, y, maxWidth, textColor, bKeepsAutomaticTextColorAsIs) { + this._doRender(drawingCtx, x, y, maxWidth, textColor, bKeepsAutomaticTextColorAsIs); return this; }; @@ -1170,12 +1173,12 @@ * @param {String} textColor */ - StringRender.prototype._doRender = function (drawingCtx, x, y, maxWidth, textColor) { + StringRender.prototype._doRender = function (drawingCtx, x, y, maxWidth, textColor, bKeepsAutomaticTextColorAsIs) { let self = this; let ctx = drawingCtx || this.drawingCtx; let zoom = ctx.getZoom(); let ppiy = ctx.getPPIY(); - this.drawState.reset(drawingCtx, textColor, this.flags, this.angle); + this.drawState.reset(drawingCtx, textColor, this.flags, this.angle, bKeepsAutomaticTextColorAsIs); let drawState = this.drawState; let align = this.getEffectiveAlign(); let i, j, p, p_, strBeg; @@ -1370,6 +1373,7 @@ this.currentFont = null; this.currentColor = null; this.textColor = null; + this.keepsAutomaticTextColorAsIs = false; this.angle = 0; this.currentLine = null; this.startIdx = 0; @@ -1407,7 +1411,17 @@ let fsz = prop.font.getSize(); let lw = asc_round(fsz * ppiy / 72 / 18) || 1; - ctx.setStrokeStyle(prop.c || textColor) + + let decorationColor = prop.c || textColor; + if (ctx.isDarkMode) { + let isDecorationRecolorable = !this.keepsAutomaticTextColorAsIs && AscCommonExcel.isColorAutomatic(decorationColor); + if (isDecorationRecolorable) { + //only modify default colored cell (the ones not explicitly colored by the user or a table template) + decorationColor = ctx.getDarkModeCorrectedColor(decorationColor.getR(), decorationColor.getG(), + decorationColor.getB(), decorationColor.getA()); + } + } + ctx.setStrokeStyle(decorationColor) .setLineWidth(lw) .beginPath(); let dy = (lw / 2); @@ -1455,6 +1469,24 @@ let _g = textColor.getG(); let _b = textColor.getB(); let _a = textColor.getA(); + + if (this.drawingCtx.isDarkMode) { + // isColorAutomatic identifies the "no color set" default (see WorkbookElems.js); + // only that should be dark-mode-inverted, never a color some cell/run actually + // picked. keepsAutomaticTextColorAsIs exempts default text too, when the + // background it sits on (the cell's own fill, or nothing at all) is already + // light enough: that background was authored with some text color pairing in + // mind, and inverting default text on top of it can turn readable-on-light into + // unreadable-on-light (e.g. white text on a light table-style band). + let isTextRecolorable = !this.keepsAutomaticTextColorAsIs && AscCommonExcel.isColorAutomatic(textColor); + if (isTextRecolorable) { + //only modify default colored cell (the ones not explicitly colored by the user or a table template) + textColor = this.drawingCtx.getDarkModeCorrectedColor(_r, _g, _b, _a); + _r = textColor.getR(); + _g = textColor.getG(); + _b = textColor.getB(); + } + } let setColor = true; if (this.drawingCtx.fillColor && this.drawingCtx.fillColor.isEqual(_r, _g, _b, _a)) { setColor = false; @@ -1518,6 +1550,20 @@ let _g = textColor.getG(); let _b = textColor.getB(); let _a = textColor.getA(); + + if (this.drawingCtx.isDarkMode) { + // see beginFragment above: use AscCommonExcel.isColorAutomatic plus + // keepsAutomaticTextColorAsIs, and only resolve/reallocate when dark mode is on + // and the color isn't explicit + let isTextRecolorable = !this.keepsAutomaticTextColorAsIs && AscCommonExcel.isColorAutomatic(textColor); + if (isTextRecolorable) { + //only modify default colored cell (the ones not explicitly colored by the user or a table template) + textColor = this.drawingCtx.getDarkModeCorrectedColor(_r, _g, _b, _a); + _r = textColor.getR(); + _g = textColor.getG(); + _b = textColor.getB(); + } + } let setColor = true; if (this.drawingCtx.fillColor && this.drawingCtx.fillColor.isEqual(_r, _g, _b, _a)) { setColor = false; @@ -1584,8 +1630,9 @@ - TableCellDrawState.prototype.reset = function(drawingCtx, textColor, flags, angle) { + TableCellDrawState.prototype.reset = function(drawingCtx, textColor, flags, angle, bKeepsAutomaticTextColorAsIs) { this.drawingCtx = drawingCtx || this.stringRender.drawingCtx; + this.keepsAutomaticTextColorAsIs = !!bKeepsAutomaticTextColorAsIs; this.x = 0; this.y = 0; this.baseY = 0; diff --git a/cell/view/WorkbookView.js b/cell/view/WorkbookView.js index 9459ddaeec..73e63e41f7 100644 --- a/cell/view/WorkbookView.js +++ b/cell/view/WorkbookView.js @@ -98,12 +98,34 @@ var rgb = parseInt(_color.split('#')[1], 16); return new CColor((rgb >> 16) & 0xFF, (rgb >> 8) & 0xFF, rgb & 0xFF); }; - this.updateStyle = function () { + this.updateStyle = function (isContentDarkMode) { this.header.style = this._generateStyle(); this.header.groupDataBorder = this.getCColor(AscCommon.GlobalSkin.GroupDataBorder); this.header.editorBorder = this.getCColor(AscCommon.GlobalSkin.EditorBorder); this.header.cornerColor = this.getCColor(AscCommon.GlobalSkin.SelectAllIcon); this.header.cornerColorSheetView = this.getCColor(AscCommon.GlobalSkin.SheetViewSelectAllIcon); + + // Cell background/grid follow the "Dark Document" (content) theme, not the + // interface theme's GlobalSkin. The two are independent: light UI with a dark + // document, and vice versa, must both be possible. + var cellSkin = AscCommon.EditorSkins[isContentDarkMode ? "theme-dark" : "theme-light"]; + this.cells.defaultState.background = this.getCColor(cellSkin.CellBackground); + this.cells.defaultState.border = this.getCColor(cellSkin.CellGrid); + // col/row resize border pattern must be regenerated for the new theme's color + this.ptrnLineDotted1 = this._generateLineDottedPattern(cellSkin.ColOrRowResizeBorderColor); + }; + + // builds the 2x2 dotted-pattern canvas used by ptrnLineDotted1 + this._generateLineDottedPattern = function (color) { + let cnv = document.createElement("canvas"); + cnv.width = 2; + cnv.height = 2; + let ctx = cnv.getContext("2d"); + ctx.clearRect(0, 0, 2, 2); + ctx.fillStyle = color; + ctx.fillRect(0, 0, 1, 1); + ctx.fillRect(1, 1, 1, 1); + return ctx.createPattern(cnv, "repeat"); }; this._generateStyle = function () { return [// Header colors @@ -147,9 +169,21 @@ printColor: new CColor(0, 0, 0) }; this.cells = { + // note, below defaultState.background colors values are for placeholder only -- WorkbookView's own constructor unconditionally overwrites both + // values via this.updateDarkMode() -> updateStyle() before it returns, in light mode + // same as dark, from EditorSkins (content theme), not GlobalSkin (interface theme, + // a different, independent axis) defaultState: { background: new CColor(255, 255, 255), border: new CColor(202, 202, 202) - }, padding: -1 /*px horizontal padding*/ + }, + // Print and print-preview must always render the light theme, regardless of whatever + // content dark mode is currently active on screen (dark mode is a screen-editing + // affordance, not a document property) - fixed, never updated by updateStyle(). + printState: { + background: this.getCColor(AscCommon.EditorSkins["theme-light"].CellBackground), + border: this.getCColor(AscCommon.EditorSkins["theme-light"].CellGrid) + }, + padding: -1 /*px horizontal padding*/ }; this.activeCellBorderColor = new CColor(72, 121, 92); this.activeCellBorderColor2 = new CColor(255, 255, 255, 1); @@ -161,15 +195,8 @@ // Число знаков для математической информации this.mathMaxDigCount = 9; - var cnv = document.createElement("canvas"); - cnv.width = 2; - cnv.height = 2; - var ctx = cnv.getContext("2d"); - ctx.clearRect(0, 0, 2, 2); - ctx.fillStyle = "#000"; - ctx.fillRect(0, 0, 1, 1); - ctx.fillRect(1, 1, 1, 1); - this.ptrnLineDotted1 = ctx.createPattern(cnv, "repeat"); + // externalise canvas-based col/rows resize border creation AND color + this.ptrnLineDotted1 = this._generateLineDottedPattern(AscCommon.EditorSkins["theme-light"].ColOrRowResizeBorderColor); this.halfSelection = false; @@ -336,6 +363,13 @@ this.isPartialReading = null; + // buffers/cellEditor already exist at this point (created inside _init() above), so + // routing through updateDarkMode here keeps the DrawingContext instances used for the + // very first paint in sync with the mode the document actually loads in, rather than + // only picking it up on the next explicit dark-mode toggle + this.isDarkMode = !!(Api && Api.isDarkMode); + this.updateDarkMode(this.isDarkMode); + return this; } @@ -4119,7 +4153,8 @@ } printPreviewContext.clear(); - printPreviewContext.setFillStyle( this.defaults.worksheetView.cells.defaultState.background ) + // Always the light theme here - print preview must not inherit content dark mode. + printPreviewContext.setFillStyle( this.defaults.worksheetView.cells.printState.background ) .fillRect( 0, 0, printPreviewContext.getWidth(), printPreviewContext.getHeight() ); var ws; @@ -5290,7 +5325,42 @@ }; WorkbookView.prototype.updateSkin = function () { - this.defaults.worksheetView.updateStyle(); + this.defaults.worksheetView.updateStyle(this.isDarkMode); + }; + + // Every DrawingContext owned by this WorkbookView: the main/overlay buffers and the + // cell editor's own contexts. Used by updateDarkMode to keep them all in sync. + WorkbookView.prototype._getOwnedDrawingContexts = function () { + var contexts = []; + if (this.buffers.main) { + contexts.push(this.buffers.main); + } + if (this.buffers.overlay) { + contexts.push(this.buffers.overlay); + } + if (this.cellEditor) { + if (this.cellEditor.drawingCtx) { + contexts.push(this.cellEditor.drawingCtx); + } + if (this.cellEditor.overlayCtx) { + contexts.push(this.cellEditor.overlayCtx); + } + } + return contexts; + }; + + WorkbookView.prototype.updateDarkMode = function (isDarkMode) { + this.isDarkMode = isDarkMode; + this.defaults.worksheetView.updateStyle(isDarkMode); + this._getOwnedDrawingContexts().forEach(function (drawingCtx) { + drawingCtx.isDarkMode = isDarkMode; + }); + for (var i in this.wsViews) { + var ws = this.wsViews[i]; + if (ws) { + ws._cleanCellsTextMetricsCache(); + } + } }; WorkbookView.prototype.executeWithCurrentTopLeftCell = function (runFunction) { diff --git a/cell/view/WorksheetView.js b/cell/view/WorksheetView.js index 156b29fe6b..4eacc2ac7c 100644 --- a/cell/view/WorksheetView.js +++ b/cell/view/WorksheetView.js @@ -5706,7 +5706,9 @@ function isAllowPasteLink(pastedWb) { //рисуем текст для преварительного просмотра //this._drawPageBreakPreviewText(drawingCtx, range, leftFieldInPx, topFieldInPx, width, height); - ctx.setStrokeStyle(this.settings.cells.defaultState.border) + // Printed gridlines must always be the light theme's, not whatever content dark mode + // is currently active on screen. + ctx.setStrokeStyle(isPrint ? this.settings.cells.printState.border : this.settings.cells.defaultState.border) .setLineWidth(1).beginPath(); let i, d, l; @@ -5735,7 +5737,7 @@ function isAllowPasteLink(pastedWb) { // Clear grid for pivot tables with classic and outline layout let clearRange, pivotRange, clearRanges = this.model.getPivotTablesClearRanges(range); - ctx.setFillStyle(this.settings.cells.defaultState.background); + ctx.setFillStyle(isPrint ? this.settings.cells.printState.background : this.settings.cells.defaultState.background); for (i = 0; i < clearRanges.length; i += 2) { clearRange = clearRanges[i]; pivotRange = clearRanges[i + 1]; @@ -6111,6 +6113,7 @@ function isAllowPasteLink(pastedWb) { }; /** Рисует фон ячеек в строке */ + /** ↪ AI Translation → “Draws the background of cells in a row” */ WorksheetView.prototype._drawRowBG = function (drawingCtx, row, colStart, colEnd, offsetX, offsetY, mergedCells, mc, cfIterator) { var height = this._getRowHeight(row); if (0 === height && mergedCells) { @@ -6156,15 +6159,27 @@ function isAllowPasteLink(pastedWb) { } //without merged -> merged after, because part of merged can - if (this.isPageBreakPreview(true) && !mc && this.pagesModeDataContains(col, row) === false) { + // isPageBreakBorderFill: this cell's fill is about to become the page-break-preview + // border overlay, already theme-resolved (not a raw color needing correction) + var isPageBreakBorderFill = this.isPageBreakPreview(true) && !mc && this.pagesModeDataContains(col, row) === false; + if (isPageBreakBorderFill) { findFillColor = this.settings.cells.defaultState.border; } if (findFillColor || hasFill || mc) { // ToDo не отрисовываем заливку границ от ячеек c заливкой, которые находятся правее и ниже // отрисовываемого диапазона. Но по факту проблем быть не должно. + /** ↪ AI Translation → “TODO: we don't draw the fill of cell borders + * for filled cells that are located to the right and below the range being drawn. + * But in practice there shouldn't be any problems.” */ var fillGrid = findFillColor || hasFill; - findFillColor = findFillColor || (!hasFill && mc && this.settings.cells.defaultState.background); + // exempt from dark-mode auto-correction unless this is a search-highlight + // override (findFillColor, read here before it's reassigned below): both the + // cell's own fill and the merge's default background set below are already correct + var keepsFillColorAsIs = !findFillColor || isPageBreakBorderFill; + // print/print-preview must use the always-light printState.background, not + // the document's dark-mode-resolved defaultState.background + findFillColor = findFillColor || (!hasFill && mc && (this.usePrintScale ? this.settings.cells.printState.background : this.settings.cells.defaultState.background)); var x = this._getColLeft(col) - (fillGrid ? 1 : 0) + this.getRightToLeftOffset(); var y = top - (fillGrid ? 1 : 0); @@ -6175,7 +6190,7 @@ function isAllowPasteLink(pastedWb) { fill = new AscCommonExcel.Fill(); fill.fromColor(findFillColor); } - AscCommonExcel.drawFillCell(ctx, graphics, fill, new AscCommon.asc_CRect((this.getRightToLeft() ? (this.getCtxWidth(ctx) - x - w + offsetX) : x - offsetX), y - offsetY, w, h)); + AscCommonExcel.drawFillCell(ctx, graphics, fill, new AscCommon.asc_CRect((this.getRightToLeft() ? (this.getCtxWidth(ctx) - x - w + offsetX) : x - offsetX), y - offsetY, w, h), keepsFillColorAsIs); } if (this.isPageBreakPreview(true) && mc) { @@ -6192,7 +6207,8 @@ function isAllowPasteLink(pastedWb) { let _fill = new AscCommonExcel.Fill(); _fill.fromColor(this.settings.cells.defaultState.border); - AscCommonExcel.drawFillCell(ctx, graphics, _fill, new AscCommon.asc_CRect((this.getRightToLeft() ? (this.getCtxWidth(ctx) - _x - w + offsetX) : _x - offsetX), _y - offsetY, _w, _h)); + // defaultState.border is already theme-resolved, same as isPageBreakBorderFill above + AscCommonExcel.drawFillCell(ctx, graphics, _fill, new AscCommon.asc_CRect((this.getRightToLeft() ? (this.getCtxWidth(ctx) - _x - w + offsetX) : _x - offsetX), _y - offsetY, _w, _h), true); } } } @@ -6365,7 +6381,7 @@ function isAllowPasteLink(pastedWb) { } else { fill.fromColor(color); } - AscCommonExcel.drawFillCell(ctx, graphics, fill, new AscCommon.asc_CRect((this.getRightToLeft() ? (this.getCtxWidth(ctx) - x - dataBarLength) : x), top, dataBarLength, height - 3)); + AscCommonExcel.drawFillCell(ctx, graphics, fill, new AscCommon.asc_CRect((this.getRightToLeft() ? (this.getCtxWidth(ctx) - x - dataBarLength) : x), top, dataBarLength, height - 3), true); var color = (isPositive || oRuleElement.NegativeBarBorderColorSameAsPositive) ? oRuleElement.BorderColor : oRuleElement.NegativeBorderColor; if (color) { @@ -6467,6 +6483,39 @@ function isAllowPasteLink(pastedWb) { return oRuleElement.ShowValue; }; + // Decides whether default/automatic text drawn on this cell should keep its color as-is + // rather than get dark-mode corrected. A cell's own fill (or the fixed search-highlight + // color) only earns that when the background is itself light enough for black text to stay + // readable on it - a dark fill still needs the correction, same as no fill at all (where + // the dark canvas shows through and always needs it). + // resolvedFallbackBg: only passed by the cell editor (openCellEditor). There, a pattern/ + // gradient fill is never actually drawn - the editor paints cells.defaultState.background + // (already dark-mode-corrected) behind the text instead, so that color is fully known and + // this can check it directly instead of falling back to the "unknown contrast" exemption + // below. The grid's real draw path (_drawCellText) never passes it, since there the + // pattern/gradient genuinely is rendered and its per-pixel contrast is genuinely unknown. + WorksheetView.prototype._getKeepsAutomaticTextColorAsIs = function (c, row, col, resolvedFallbackBg) { + var isFindResult = this.handlers.trigger('selectSearchingResults') && undefined !== this.workbook.inFindResults(this, row, col); + if (isFindResult) { + var findColor = this.settings.findFillColor; + return !AscCommon.isColorDark(findColor.getR(), findColor.getG(), findColor.getB()); + } + var fill = c.getFill(); + if (!fill.hasFill()) { + return false; + } + var solidFill = fill.getSolidFill(); + if (!solidFill) { + if (resolvedFallbackBg) { + return !AscCommon.isColorDark(resolvedFallbackBg.getR(), resolvedFallbackBg.getG(), resolvedFallbackBg.getB()); + } + // pattern/gradient fill: no single background color to check against, keep the + // pre-existing conservative behavior of exempting default text from correction + return true; + } + return !AscCommon.isColorDark(solidFill.getR(), solidFill.getG(), solidFill.getB()); + }; + /** Рисует текст ячейки */ WorksheetView.prototype._drawCellText = function (drawingCtx, cfIterator, col, row, colStart, colEnd, offsetX, offsetY) { var ct = this._getCellTextCache(col, row); @@ -6488,7 +6537,6 @@ function isAllowPasteLink(pastedWb) { var color = font.getColor(); var isMerged = ct.flags.isMerged(), range, isWrapped = ct.flags.wrapText; var ctx = drawingCtx || this.drawingCtx; - if (isMerged) { range = ct.flags.merged; if (col !== range.c1 || row !== range.r1) { @@ -6496,6 +6544,13 @@ function isAllowPasteLink(pastedWb) { } } + // see _getKeepsAutomaticTextColorAsIs: only a light fill (or the fixed yellow + // search highlight) exempts default text from dark-mode inversion; a dark fill still needs it. + // Only consulted when this draw target is in dark mode, so skip computing it otherwise. + //computed at cell level to avoid per character at the cost of passing in parameter + //computed only fordarkmode for now -> = on darkbackground invert text color + var keepsAutomaticTextColorAsIs = ctx.isDarkMode ? this._getKeepsAutomaticTextColorAsIs(c, row, col) : true; + var colL = isMerged ? range.c1 : Math.max(colStart, col - ct.sideL); var colR = isMerged ? Math.min(range.c2, this.nColsCount - 1) : Math.min(colEnd, col + ct.sideR); var rowT = isMerged ? range.r1 : row; @@ -6659,7 +6714,7 @@ function isAllowPasteLink(pastedWb) { } } - this._drawText(this.stringRender, drawingCtx, 0, 0, textW, color, true); + this._drawText(this.stringRender, drawingCtx, 0, 0, textW, color, true, keepsAutomaticTextColorAsIs); this.stringRender.resetTransform(isPrintPreview ? null : drawingCtx); if (transformMatrix) { @@ -6713,7 +6768,7 @@ function isAllowPasteLink(pastedWb) { } } - this._drawText(this.stringRender.restoreInternalState(ct.state), ctx, textX, textY, textW, color); + this._drawText(this.stringRender.restoreInternalState(ct.state), ctx, textX, textY, textW, color, false, keepsAutomaticTextColorAsIs); this._RemoveClipRect(ctx); } @@ -7404,7 +7459,9 @@ function isAllowPasteLink(pastedWb) { let rtlKf = this.getRightToLeft() ? -1 : 1; var nextCell = -1; var ctx = drawingCtx || this.drawingCtx; - ctx.setFillStyle( this.settings.cells.defaultState.background ); + // Printed fill must always be the light theme's, not whatever content dark mode + // is currently active on screen. + ctx.setFillStyle( this.usePrintScale ? this.settings.cells.printState.background : this.settings.cells.defaultState.background ); for ( var col = colBeg; col < colEnd; ++col ) { var c = -1 !== nextCell ? nextCell : this._getCell( col, row ); var bg = null !== c ? c.getFillColor() : null; @@ -7466,7 +7523,17 @@ function isAllowPasteLink(pastedWb) { if (isNewColor) { bc = border.getColorOrDefault(); - ctx.setStrokeStyle(bc); + // bc itself must stay the raw color + var colorToDraw = bc; + + if (ctx.isDarkMode) { + var isBorderRecolorable = AscCommonExcel.isColorAutomatic(bc); + if (isBorderRecolorable) { + //don't have explicit border colors -> modify it + colorToDraw = ctx.getDarkModeCorrectedColor(bc.getR(), bc.getG(), bc.getB(), bc.getA()); + } + } + ctx.setStrokeStyle(colorToDraw); } if (isNewStyle) { bs = border.s; @@ -19536,12 +19603,22 @@ function isAllowPasteLink(pastedWb) { this.model.workbook.handlers.trigger("cleanCutData", true, true); this.model.workbook.handlers.trigger("cleanCopyData", true); + var resolvedBg = bg || this.settings.cells.defaultState.background; editor.open({ enterOptions: enterOptions, fragments: fragments, flags: fl, font: font, - background: bg || this.settings.cells.defaultState.background, + background: resolvedBg, + // same rule StringRender uses for the grid, see _getKeepsAutomaticTextColorAsIs: + // a cell's own fill only earns this when that fill is light enough for black + // text to stay readable on it. Only consulted in dark mode, so skip computing it otherwise. + // bg is non-null for a genuine pattern fill too (its foreground color, see + // Fill.prototype.bg), and that's exactly what gets painted raw as the background + // above - pass the same value through so contrast is checked against what's actually + // rendered, not the grid's "unknown contrast" exemption (that exemption is for the + // grid's real per-pixel pattern rendering, which this editor never does). + keepsAutomaticTextColorAsIs: this.drawingCtx.isDarkMode ? this._getKeepsAutomaticTextColorAsIs(c, row, col, resolvedBg) : true, zoom: this.getZoom(), isAddPersentFormat: enterOptions.quickInput && Asc.c_oAscNumFormatType.Percent === c.getNumFormatType(), autoComplete: arrAutoComplete, @@ -27848,8 +27925,8 @@ function isAllowPasteLink(pastedWb) { ctx.clearRectByX(this.getRightToLeft() ? (this.getCtxWidth(ctx) - x - w) : x, y, w, h); return ctx; }; - WorksheetView.prototype._drawText = function (stringRender, ctx, textX, textY, textW, color, skipRtl) { - stringRender.render(ctx, this.getRightToLeft() && !skipRtl ? (this.getCtxWidth(ctx) - textX - textW) : textX, textY, textW, color); + WorksheetView.prototype._drawText = function (stringRender, ctx, textX, textY, textW, color, skipRtl, bKeepsAutomaticTextColorAsIs) { + stringRender.render(ctx, this.getRightToLeft() && !skipRtl ? (this.getCtxWidth(ctx) - textX - textW) : textX, textY, textW, color, bKeepsAutomaticTextColorAsIs); return stringRender; }; WorksheetView.prototype._fillText = function (ctx, text, x, y, maxWidth, charWidths, angle) { diff --git a/common/Drawings/GraphicsBase.js b/common/Drawings/GraphicsBase.js index d49d017661..5689109e25 100644 --- a/common/Drawings/GraphicsBase.js +++ b/common/Drawings/GraphicsBase.js @@ -55,6 +55,19 @@ AscFormat.CColorModifiers.prototype.HSL2RGB(oHSL, oRGB, true); return oRGB; }; + // Is this color dark enough that light (rather than black) text/decoration is needed on + // top of it? Uses HSL lightness (RGB2HSL's L, 0-255) rather than a weighted-luma formula + // (e.g. ITU-R BT.601) to stay consistent with darkModeCorrectColor2 just above, which + // already treats HSL L as this codebase's definition of "how light is this color" for + // dark-mode purposes - a color classified "dark" here should be exactly the kind of color + // darkModeCorrectColor2 would brighten. + AscCommon.isColorDark = function(r, g, b) + { + var oHSL = {}; + AscFormat.CColorModifiers.prototype.RGB2HSL(r, g, b, oHSL); + //arbitrary choosen 100 by experience instead of theorical 128; + return oHSL.L < 100; + }; AscCommon.RendererType = { Base : 0, diff --git a/common/skin.js b/common/skin.js index 668a3dd047..4a3bf672a2 100644 --- a/common/skin.js +++ b/common/skin.js @@ -169,6 +169,11 @@ var EditorSkins = { SheetViewCellBackgroundHover : "#97e3b6", SheetViewCellTitleLabel : "#121212", + CellBackground : "#FFFFFF", + CellGrid : "#CACACA", + + ColOrRowResizeBorderColor : "#000000", //spreadsheeteditor - col/row resize guides + ColorDark : "#ffffff", ColorDarkActive : "#ffffff", ColorDarkHighlighted : "#c1c1c1", @@ -318,6 +323,11 @@ var EditorSkins = { SheetViewCellBackgroundHover : "#97e3b6", SheetViewCellTitleLabel : "#121212", + CellBackground : "#262626", + CellGrid : "#454545", + + ColOrRowResizeBorderColor : "#CCCCCC", //spreadsheeteditor - col/row resize guides + ColorDark : "#333", ColorDarkActive : "#333", ColorDarkHighlighted : "#333", @@ -491,7 +501,12 @@ function updateGlobalSkinColors(theme) continue; if ("" === colorMap[color]) continue; - if (undefined === theme[colorMap[color]]) + // theme[colorMap[color]] resolving to "" (not just undefined) must be checked too: + // some caller-supplied theme objects derive from CSS custom properties (web-apps' + // Themes.js), where an undefined property reads back as "" (never undefined) -- + // and correctColor("") would otherwise silently resolve to solid black instead of + // leaving GlobalSkin[color] alone + if (undefined === theme[colorMap[color]] || "" === theme[colorMap[color]]) continue; if(typeof GlobalSkin[color] === "number") @@ -547,6 +562,7 @@ function updateGlobalSkin(obj) window['AscCommon'] = window['AscCommon'] || {}; window['AscCommon'].GlobalSkin = GlobalSkin; window['AscCommon'].updateGlobalSkin = updateGlobalSkin; +window['AscCommon'].EditorSkins = EditorSkins; window['AscCommon'].RgbaHexToRGBA = function(color) {