From 3486111722296f287158e0340789c607642c1067 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Fri, 17 May 2024 01:41:48 +0200 Subject: [PATCH 1/4] AtlasEngine: Implement remaining underlines and builtin glyphs for D2D (#17278) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This implements builtin glyphs for our Direct2D renderer, as well as dashed and curly underlines. With this in place the only two features it doesn't support are inverted cursors and VT soft fonts. This allows us to remove the `_hack*` members introduced in a6a0e44. The implementation of dashed underlines is trivial, while curly underlines use quadratic bezier curves. Caching the curve as a sprite is possible, however I feel like that can be done in the future. Builtin glyphs on the other hand require a cache, because otherwise filling the entire viewport with shaded glyphs would result in poor performance. This is why it's built on top of `ID2D1SpriteBatch`. Unfortunately the API causes an eager flush of other pending graphics instructions, which is why there's still a decent perf hit. Finally, as a little extra, this fixes the rounded powerline glyph shapes being slightly cut off. The fix is to simply don't round the position and radius of the ellipsis/semi-circle. Closes #17224 ## Validation Steps Performed * RenderingTests.exe updated ✅ * All supported builtin glyphs look sorta right at different sizes ✅ --- src/renderer/atlas/AtlasEngine.cpp | 14 +- src/renderer/atlas/AtlasEngine.h | 12 - src/renderer/atlas/AtlasEngine.r.cpp | 8 +- src/renderer/atlas/BackendD2D.cpp | 370 ++++++++++++++++++++++----- src/renderer/atlas/BackendD2D.h | 15 +- src/renderer/atlas/BackendD3D.cpp | 32 ++- src/renderer/atlas/BuiltinGlyphs.cpp | 59 +++-- src/renderer/atlas/BuiltinGlyphs.h | 12 +- src/tools/RenderingTests/main.cpp | 60 ++--- 9 files changed, 422 insertions(+), 160 deletions(-) diff --git a/src/renderer/atlas/AtlasEngine.cpp b/src/renderer/atlas/AtlasEngine.cpp index 28295aeadd2..b5a89f6c09d 100644 --- a/src/renderer/atlas/AtlasEngine.cpp +++ b/src/renderer/atlas/AtlasEngine.cpp @@ -74,9 +74,8 @@ try _handleSettingsUpdate(); } - if (ATLAS_DEBUG_DISABLE_PARTIAL_INVALIDATION || _hackTriggerRedrawAll) + if constexpr (ATLAS_DEBUG_DISABLE_PARTIAL_INVALIDATION) { - _hackTriggerRedrawAll = false; _api.invalidatedRows = invalidatedRowsAll; _api.scrollOffset = 0; } @@ -703,8 +702,6 @@ void AtlasEngine::_recreateFontDependentResources() _api.textFormatAxes[i] = { fontAxisValues.data(), fontAxisValues.size() }; } } - - _hackWantsBuiltinGlyphs = _p.s->font->builtinGlyphs && !_hackIsBackendD2D; } void AtlasEngine::_recreateCellCountDependentResources() @@ -765,18 +762,13 @@ void AtlasEngine::_flushBufferLine() // This would seriously blow us up otherwise. Expects(_api.bufferLineColumn.size() == _api.bufferLine.size() + 1); + const auto builtinGlyphs = _p.s->font->builtinGlyphs; const auto beg = _api.bufferLine.data(); const auto len = _api.bufferLine.size(); size_t segmentBeg = 0; size_t segmentEnd = 0; bool custom = false; - if (!_hackWantsBuiltinGlyphs) - { - _mapRegularText(0, len); - return; - } - while (segmentBeg < len) { segmentEnd = segmentBeg; @@ -789,7 +781,7 @@ void AtlasEngine::_flushBufferLine() codepoint = til::combine_surrogates(codepoint, beg[i++]); } - const auto c = BuiltinGlyphs::IsBuiltinGlyph(codepoint) || BuiltinGlyphs::IsSoftFontChar(codepoint); + const auto c = (builtinGlyphs && BuiltinGlyphs::IsBuiltinGlyph(codepoint)) || BuiltinGlyphs::IsSoftFontChar(codepoint); if (custom != c) { break; diff --git a/src/renderer/atlas/AtlasEngine.h b/src/renderer/atlas/AtlasEngine.h index 1f26644ebe6..ccb4da9fb4e 100644 --- a/src/renderer/atlas/AtlasEngine.h +++ b/src/renderer/atlas/AtlasEngine.h @@ -127,18 +127,6 @@ namespace Microsoft::Console::Render::Atlas std::unique_ptr _b; RenderingPayload _p; - // _p.s->font->builtinGlyphs is the setting which decides whether we should map box drawing glyphs to - // our own builtin versions. There's just one problem: BackendD2D doesn't have this functionality. - // But since AtlasEngine shapes the text before it's handed to the backends, it would need to know - // whether BackendD2D is in use, before BackendD2D even exists. These two flags solve the issue - // by triggering a complete, immediate redraw whenever the backend type changes. - // - // The proper solution is to move text shaping into the backends. - // Someone just needs to write a generic "TextBuffer to DWRITE_GLYPH_RUN" function. - bool _hackIsBackendD2D = false; - bool _hackWantsBuiltinGlyphs = true; - bool _hackTriggerRedrawAll = false; - struct ApiState { GenerationalSettings s = DirtyGenerationalSettings(); diff --git a/src/renderer/atlas/AtlasEngine.r.cpp b/src/renderer/atlas/AtlasEngine.r.cpp index a0dbdcc5470..3591abe7905 100644 --- a/src/renderer/atlas/AtlasEngine.r.cpp +++ b/src/renderer/atlas/AtlasEngine.r.cpp @@ -77,7 +77,7 @@ CATCH_RETURN() [[nodiscard]] bool AtlasEngine::RequiresContinuousRedraw() noexcept { - return ATLAS_DEBUG_CONTINUOUS_REDRAW || (_b && _b->RequiresContinuousRedraw()) || _hackTriggerRedrawAll; + return ATLAS_DEBUG_CONTINUOUS_REDRAW || (_b && _b->RequiresContinuousRedraw()); } void AtlasEngine::WaitUntilCanRender() noexcept @@ -282,21 +282,15 @@ void AtlasEngine::_recreateBackend() { case GraphicsAPI::Direct2D: _b = std::make_unique(); - _hackIsBackendD2D = true; break; default: _b = std::make_unique(_p); - _hackIsBackendD2D = false; break; } // This ensures that the backends redraw their entire viewports whenever a new swap chain is created, // EVEN IF we got called when no actual settings changed (i.e. rendering failure, etc.). _p.MarkAllAsDirty(); - - const auto hackWantsBuiltinGlyphs = _p.s->font->builtinGlyphs && !_hackIsBackendD2D; - _hackTriggerRedrawAll = _hackWantsBuiltinGlyphs != hackWantsBuiltinGlyphs; - _hackWantsBuiltinGlyphs = hackWantsBuiltinGlyphs; } void AtlasEngine::_handleSwapChainUpdate() diff --git a/src/renderer/atlas/BackendD2D.cpp b/src/renderer/atlas/BackendD2D.cpp index 5a663b49d04..cf7bd8a2bec 100644 --- a/src/renderer/atlas/BackendD2D.cpp +++ b/src/renderer/atlas/BackendD2D.cpp @@ -4,6 +4,8 @@ #include "pch.h" #include "BackendD2D.h" +#include + #if ATLAS_DEBUG_SHOW_DIRTY #include "colorbrewer.h" #endif @@ -94,11 +96,15 @@ void BackendD2D::_handleSettingsUpdate(const RenderingPayload& p) .dpiY = static_cast(p.s->font->dpi), }; // ID2D1RenderTarget and ID2D1DeviceContext are the same and I'm tired of pretending they're not. - THROW_IF_FAILED(p.d2dFactory->CreateDxgiSurfaceRenderTarget(surface.get(), &props, reinterpret_cast(_renderTarget.addressof()))); - _renderTarget.try_query_to(_renderTarget4.addressof()); + THROW_IF_FAILED(p.d2dFactory->CreateDxgiSurfaceRenderTarget(surface.get(), &props, reinterpret_cast(_renderTarget.put()))); _renderTarget->SetUnitMode(D2D1_UNIT_MODE_PIXELS); - _renderTarget->SetAntialiasMode(D2D1_ANTIALIAS_MODE_ALIASED); + + _renderTarget.try_query_to(_renderTarget4.put()); + if (_renderTarget4) + { + THROW_IF_FAILED(_renderTarget4->CreateSpriteBatch(_builtinGlyphBatch.put())); + } } { static constexpr D2D1_COLOR_F color{}; @@ -108,18 +114,15 @@ void BackendD2D::_handleSettingsUpdate(const RenderingPayload& p) } } - if (!_dottedStrokeStyle) - { - static constexpr D2D1_STROKE_STYLE_PROPERTIES props{ .dashStyle = D2D1_DASH_STYLE_CUSTOM }; - static constexpr FLOAT dashes[2]{ 1, 1 }; - THROW_IF_FAILED(p.d2dFactory->CreateStrokeStyle(&props, &dashes[0], 2, _dottedStrokeStyle.addressof())); - } - if (renderTargetChanged || fontChanged) { const auto dpi = static_cast(p.s->font->dpi); _renderTarget->SetDpi(dpi, dpi); _renderTarget->SetTextAntialiasMode(static_cast(p.s->font->antialiasingMode)); + + _builtinGlyphsRenderTarget.reset(); + _builtinGlyphsBitmap.reset(); + _builtinGlyphsRenderTargetActive = false; } if (renderTargetChanged || fontChanged || cellCountChanged) @@ -199,6 +202,12 @@ void BackendD2D::_drawText(RenderingPayload& p) for (const auto& m : row->mappings) { + if (!m.fontFace) + { + baselineX = _drawBuiltinGlyphs(p, row, m, baselineY, baselineX); + continue; + } + const auto colorsBegin = row->colors.begin(); auto it = colorsBegin + m.glyphsFrom; const auto end = colorsBegin + m.glyphsTo; @@ -228,42 +237,39 @@ void BackendD2D::_drawText(RenderingPayload& p) baselineY, }; - if (glyphRun.fontFace) + D2D1_RECT_F bounds = GlyphRunEmptyBounds; + wil::com_ptr enumerator; + + if (p.s->font->colorGlyphs) { - D2D1_RECT_F bounds = GlyphRunEmptyBounds; - wil::com_ptr enumerator; + enumerator = TranslateColorGlyphRun(p.dwriteFactory4.get(), baselineOrigin, &glyphRun); + } - if (p.s->font->colorGlyphs) + if (enumerator) + { + while (ColorGlyphRunMoveNext(enumerator.get())) { - enumerator = TranslateColorGlyphRun(p.dwriteFactory4.get(), baselineOrigin, &glyphRun); + const auto colorGlyphRun = ColorGlyphRunGetCurrentRun(enumerator.get()); + ColorGlyphRunDraw(_renderTarget4.get(), _emojiBrush.get(), brush, colorGlyphRun); + ColorGlyphRunAccumulateBounds(_renderTarget.get(), colorGlyphRun, bounds); } + } + else + { + _renderTarget->DrawGlyphRun(baselineOrigin, &glyphRun, brush, DWRITE_MEASURING_MODE_NATURAL); + GlyphRunAccumulateBounds(_renderTarget.get(), baselineOrigin, &glyphRun, bounds); + } - if (enumerator) - { - while (ColorGlyphRunMoveNext(enumerator.get())) - { - const auto colorGlyphRun = ColorGlyphRunGetCurrentRun(enumerator.get()); - ColorGlyphRunDraw(_renderTarget4.get(), _emojiBrush.get(), brush, colorGlyphRun); - ColorGlyphRunAccumulateBounds(_renderTarget.get(), colorGlyphRun, bounds); - } - } - else + if (bounds.top < bounds.bottom) + { + // Since we used SetUnitMode(D2D1_UNIT_MODE_PIXELS), bounds.top/bottom is in pixels already and requires no conversion/rounding. + if (row->lineRendition != LineRendition::DoubleHeightTop) { - _renderTarget->DrawGlyphRun(baselineOrigin, &glyphRun, brush, DWRITE_MEASURING_MODE_NATURAL); - GlyphRunAccumulateBounds(_renderTarget.get(), baselineOrigin, &glyphRun, bounds); + row->dirtyBottom = std::max(row->dirtyBottom, static_cast(lrintf(bounds.bottom))); } - - if (bounds.top < bounds.bottom) + if (row->lineRendition != LineRendition::DoubleHeightBottom) { - // Since we used SetUnitMode(D2D1_UNIT_MODE_PIXELS), bounds.top/bottom is in pixels already and requires no conversion/rounding. - if (row->lineRendition != LineRendition::DoubleHeightTop) - { - row->dirtyBottom = std::max(row->dirtyBottom, static_cast(lrintf(bounds.bottom))); - } - if (row->lineRendition != LineRendition::DoubleHeightBottom) - { - row->dirtyTop = std::min(row->dirtyTop, static_cast(lrintf(bounds.top))); - } + row->dirtyTop = std::min(row->dirtyTop, static_cast(lrintf(bounds.top))); } } @@ -274,6 +280,8 @@ void BackendD2D::_drawText(RenderingPayload& p) } } + _flushBuiltinGlyphs(); + if (!row->gridLineRanges.empty()) { _drawGridlineRow(p, row, y); @@ -300,6 +308,160 @@ void BackendD2D::_drawText(RenderingPayload& p) } } +f32 BackendD2D::_drawBuiltinGlyphs(const RenderingPayload& p, const ShapedRow* row, const FontMapping& m, f32 baselineY, f32 baselineX) +{ + const f32 cellTop = baselineY - p.s->font->baseline; + const f32 cellBottom = cellTop + p.s->font->cellSize.y; + const f32 cellWidth = p.s->font->cellSize.x; + + _prepareBuiltinGlyphRenderTarget(p); + + for (size_t i = m.glyphsFrom; i < m.glyphsTo; ++i) + { + // This code runs when fontFace == nullptr. This is only the case for builtin glyphs which then use the glyphIndices + // to store UTF16 code points. In other words, this doesn't accidentally corrupt any actual glyph indices. + u32 ch = row->glyphIndices[i]; + if (til::is_leading_surrogate(ch)) + { + i += 1; + ch = til::combine_surrogates(ch, row->glyphIndices[i]); + } + + // If we don't have support for ID2D1SpriteBatch we don't support builtin glyphs. + // But we do still need to account for the glyphAdvances, which is why we can't just skip everything. + // It's very unlikely for a target device to not support ID2D1SpriteBatch as it's very old at this point. + if (_builtinGlyphBatch) + { + if (const auto off = BuiltinGlyphs::GetBitmapCellIndex(ch); off >= 0) + { + const D2D1_RECT_F dst{ baselineX, cellTop, baselineX + cellWidth, cellBottom }; + const auto src = _prepareBuiltinGlyph(p, ch, off); + const auto color = colorFromU32(row->colors[i]); + THROW_IF_FAILED(_builtinGlyphBatch->AddSprites(1, &dst, &src, &color, nullptr, sizeof(D2D1_RECT_F), sizeof(D2D1_RECT_U), sizeof(D2D1_COLOR_F), sizeof(D2D1_MATRIX_3X2_F))); + } + } + + baselineX += row->glyphAdvances[i]; + } + + return baselineX; +} + +void BackendD2D::_prepareBuiltinGlyphRenderTarget(const RenderingPayload& p) +{ + // If we don't have support for ID2D1SpriteBatch none of the related members will be initialized or used. + // We can just early-return in that case. + if (!_builtinGlyphBatch) + { + return; + } + + // If the render target is already created, all of the below has already been done in a previous frame. + // Once the relevant settings change for some reason (primarily the font->cellSize), then _handleSettingsUpdate() + // will reset the render target which will cause us to skip this condition and re-initialize it below. + if (_builtinGlyphsRenderTarget) + { + return; + } + + const auto cellWidth = static_cast(p.s->font->cellSize.x); + const auto cellHeight = static_cast(p.s->font->cellSize.y); + const auto cellArea = cellWidth * cellHeight; + const auto area = cellArea * BuiltinGlyphs::TotalCharCount; + + // This block of code calculates the size of a power-of-2 texture that has an area larger than the given `area`. + // For instance, for an area of 985x1946 = 1916810 it would result in a u/v of 2048x1024 (area = 2097152). + // We throw the "v" in this case away, because we don't really need power-of-2 textures here, + // but you can find the complete code over in BackendD3D. If someone deleted it in the meantime: + // const auto index = bitness_of_area_minus_1 - std::countl_zero(area - 1); // aka: _BitScanReverse + // const auto u = 1u << ((index + 2) / 2); + // const auto v = 1u << ((index + 1) / 2); + unsigned long index; + _BitScanReverse(&index, area - 1); + const auto potWidth = 1u << ((index + 2) / 2); + + const auto cellCountU = potWidth / cellWidth; + const auto cellCountV = (BuiltinGlyphs::TotalCharCount + cellCountU - 1) / cellCountU; + const auto u = cellCountU * cellWidth; + const auto v = cellCountV * cellHeight; + + const D2D1_SIZE_F sizeF{ static_cast(u), static_cast(v) }; + const D2D1_SIZE_U sizeU{ gsl::narrow_cast(u), gsl::narrow_cast(v) }; + static constexpr D2D1_PIXEL_FORMAT format{ DXGI_FORMAT_A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED }; + wil::com_ptr target; + THROW_IF_FAILED(_renderTarget->CreateCompatibleRenderTarget(&sizeF, &sizeU, &format, D2D1_COMPATIBLE_RENDER_TARGET_OPTIONS_NONE, target.addressof())); + + THROW_IF_FAILED(target->GetBitmap(_builtinGlyphsBitmap.put())); + _builtinGlyphsRenderTarget = target.query(); + _builtinGlyphsBitmapCellCountU = cellCountU; + _builtinGlyphsRenderTargetActive = false; + memset(&_builtinGlyphsReady[0], 0, sizeof(_builtinGlyphsReady)); +} + +D2D1_RECT_U BackendD2D::_prepareBuiltinGlyph(const RenderingPayload& p, char32_t ch, u32 off) +{ + const u32 w = p.s->font->cellSize.x; + const u32 h = p.s->font->cellSize.y; + const u32 l = (off % _builtinGlyphsBitmapCellCountU) * w; + const u32 t = (off / _builtinGlyphsBitmapCellCountU) * h; + D2D1_RECT_U rectU{ l, t, l + w, t + h }; + + // Check if we previously cached this glyph already. + if (_builtinGlyphsReady[off]) + { + return rectU; + } + + static constexpr D2D1_COLOR_F shadeColorMap[] = { + { 1, 1, 1, 0.25f }, // Shape_Filled025 + { 1, 1, 1, 0.50f }, // Shape_Filled050 + { 1, 1, 1, 0.75f }, // Shape_Filled075 + { 1, 1, 1, 1.00f }, // Shape_Filled100 + }; + + if (!_builtinGlyphsRenderTargetActive) + { + _builtinGlyphsRenderTarget->BeginDraw(); + _builtinGlyphsRenderTargetActive = true; + } + + const auto brush = _brushWithColor(0xffffffff); + const D2D1_RECT_F rectF{ + static_cast(rectU.left), + static_cast(rectU.top), + static_cast(rectU.right), + static_cast(rectU.bottom), + }; + BuiltinGlyphs::DrawBuiltinGlyph(p.d2dFactory.get(), _builtinGlyphsRenderTarget.get(), brush, shadeColorMap, rectF, ch); + + _builtinGlyphsReady[off] = true; + return rectU; +} + +void BackendD2D::_flushBuiltinGlyphs() +{ + // If we don't have support for ID2D1SpriteBatch none of the related members will be initialized or used. + // We can just early-return in that case. + if (!_builtinGlyphBatch) + { + return; + } + + if (_builtinGlyphsRenderTargetActive) + { + THROW_IF_FAILED(_builtinGlyphsRenderTarget->EndDraw()); + _builtinGlyphsRenderTargetActive = false; + } + + if (const auto count = _builtinGlyphBatch->GetSpriteCount(); count > 0) + { + _renderTarget4->SetAntialiasMode(D2D1_ANTIALIAS_MODE_ALIASED); + _renderTarget4->DrawSpriteBatch(_builtinGlyphBatch.get(), 0, count, _builtinGlyphsBitmap.get(), D2D1_BITMAP_INTERPOLATION_MODE_NEAREST_NEIGHBOR, D2D1_SPRITE_OPTIONS_NONE); + _renderTarget4->SetAntialiasMode(D2D1_ANTIALIAS_MODE_PER_PRIMITIVE); + _builtinGlyphBatch->Clear(); + } +} + f32 BackendD2D::_drawTextPrepareLineRendition(const RenderingPayload& p, const ShapedRow* row, f32 baselineY) const noexcept { const auto lineRendition = row->lineRendition; @@ -410,44 +572,118 @@ f32r BackendD2D::_getGlyphRunDesignBounds(const DWRITE_GLYPH_RUN& glyphRun, f32 void BackendD2D::_drawGridlineRow(const RenderingPayload& p, const ShapedRow* row, u16 y) { - const auto widthShift = gsl::narrow_cast(row->lineRendition != LineRendition::SingleWidth); - const auto cellSize = p.s->font->cellSize; - const auto rowTop = gsl::narrow_cast(cellSize.y * y); - const auto rowBottom = gsl::narrow_cast(rowTop + cellSize.y); - const auto textCellCenter = row->lineRendition == LineRendition::DoubleHeightTop ? rowBottom : rowTop; + const auto cellWidth = static_cast(p.s->font->cellSize.x); + const auto cellHeight = static_cast(p.s->font->cellSize.y); + const auto rowTop = cellHeight * y; + const auto rowBottom = rowTop + cellHeight; + const auto cellCenter = row->lineRendition == LineRendition::DoubleHeightTop ? rowBottom : rowTop; + const auto scaleHorizontal = row->lineRendition != LineRendition::SingleWidth ? 0.5f : 1.0f; + const auto scaledCellWidth = cellWidth * scaleHorizontal; const auto appendVerticalLines = [&](const GridLineRange& r, FontDecorationPosition pos) { - const auto from = r.from >> widthShift; - const auto to = r.to >> widthShift; + const auto from = r.from * scaledCellWidth; + const auto to = r.to * scaledCellWidth; + auto x = from + pos.position; - auto posX = from * cellSize.x + pos.position; - const auto end = to * cellSize.x; - - D2D1_POINT_2F point0{ 0, static_cast(textCellCenter) }; - D2D1_POINT_2F point1{ 0, static_cast(textCellCenter + cellSize.y) }; + D2D1_POINT_2F point0{ 0, cellCenter }; + D2D1_POINT_2F point1{ 0, cellCenter + cellHeight }; const auto brush = _brushWithColor(r.gridlineColor); const f32 w = pos.height; const f32 hw = w * 0.5f; - for (; posX < end; posX += cellSize.x) + for (; x < to; x += cellWidth) { - const auto centerX = posX + hw; + const auto centerX = x + hw; point0.x = centerX; point1.x = centerX; _renderTarget->DrawLine(point0, point1, brush, w, nullptr); } }; const auto appendHorizontalLine = [&](const GridLineRange& r, FontDecorationPosition pos, ID2D1StrokeStyle* strokeStyle, const u32 color) { - const auto from = r.from >> widthShift; - const auto to = r.to >> widthShift; + const auto from = r.from * scaledCellWidth; + const auto to = r.to * scaledCellWidth; const auto brush = _brushWithColor(color); const f32 w = pos.height; - const f32 centerY = textCellCenter + pos.position + w * 0.5f; - const D2D1_POINT_2F point0{ static_cast(from * cellSize.x), centerY }; - const D2D1_POINT_2F point1{ static_cast(to * cellSize.x), centerY }; + const f32 centerY = cellCenter + pos.position + w * 0.5f; + const D2D1_POINT_2F point0{ from, centerY }; + const D2D1_POINT_2F point1{ to, centerY }; _renderTarget->DrawLine(point0, point1, brush, w, strokeStyle); }; + const auto appendCurlyLine = [&](const GridLineRange& r) { + const auto& font = *p.s->font; + + const auto duTop = static_cast(font.doubleUnderline[0].position); + const auto duBottom = static_cast(font.doubleUnderline[1].position); + // The double-underline height is also our target line width. + const auto duHeight = static_cast(font.doubleUnderline[0].height); + + // This gives it the same position and height as our double-underline. There's no particular reason for that, apart from + // it being simple to implement and robust against more peculiar fonts with unusually large/small descenders, etc. + // We still need to ensure though that it doesn't clip out of the cellHeight at the bottom, which is why `position` has a min(). + const auto height = std::max(3.0f, duBottom + duHeight - duTop); + const auto position = std::min(duTop, cellHeight - height - duHeight); + + // The amplitude of the wave needs to account for the stroke width, so that the final height including + // antialiasing isn't larger than our target `height`. That's why we calculate `(height - duHeight)`. + // + // In other words, Direct2D draws strokes centered on the path. This also means that (for instance) + // for a line width of 1px, we need to ensure that the amplitude passes through the center of a pixel. + // Because once the path gets stroked, it'll occupy half a pixel on either side of the path. + // This results in a "crisp" look. That's why we do `round(amp + half) - half`. + const auto halfLineWidth = 0.5f * duHeight; + const auto amplitude = roundf((height - duHeight) * 0.5f + halfLineWidth) - halfLineWidth; + // While the amplitude needs to account for the stroke width, the vertical center of the wave needs + // to be at an integer pixel position of course. Otherwise, the wave won't be vertically symmetric. + const auto center = cellCenter + position + amplitude + halfLineWidth; + + const auto top = center - 2.0f * amplitude; + const auto bottom = center + 2.0f * amplitude; + const auto step = 0.5f * height; + const auto period = 4.0f * step; + + const auto from = r.from * scaledCellWidth; + const auto to = r.to * scaledCellWidth; + // Align the start of the wave to the nearest preceding period boundary. + // This ensures that the wave is continuous across color and cell changes. + auto x = floorf(from / period) * period; + + wil::com_ptr geometry; + THROW_IF_FAILED(p.d2dFactory->CreatePathGeometry(geometry.addressof())); + + wil::com_ptr sink; + THROW_IF_FAILED(geometry->Open(sink.addressof())); + + // This adds complete periods of the wave until we reach the end of the range. + sink->BeginFigure({ x, center }, D2D1_FIGURE_BEGIN_HOLLOW); + for (D2D1_QUADRATIC_BEZIER_SEGMENT segment; x < to;) + { + x += step; + segment.point1.x = x; + segment.point1.y = top; + x += step; + segment.point2.x = x; + segment.point2.y = center; + sink->AddQuadraticBezier(&segment); + + x += step; + segment.point1.x = x; + segment.point1.y = bottom; + x += step; + segment.point2.x = x; + segment.point2.y = center; + sink->AddQuadraticBezier(&segment); + } + sink->EndFigure(D2D1_FIGURE_END_OPEN); + + THROW_IF_FAILED(sink->Close()); + + const auto brush = _brushWithColor(r.underlineColor); + const D2D1_RECT_F clipRect{ from, rowTop, to, rowBottom }; + _renderTarget->PushAxisAlignedClip(&clipRect, D2D1_ANTIALIAS_MODE_ALIASED); + _renderTarget->DrawGeometry(geometry.get(), brush, duHeight, nullptr); + _renderTarget->PopAxisAlignedClip(); + }; for (const auto& r : row->gridLineRanges) { @@ -481,8 +717,28 @@ void BackendD2D::_drawGridlineRow(const RenderingPayload& p, const ShapedRow* ro } else if (r.lines.any(GridLines::DottedUnderline, GridLines::HyperlinkUnderline)) { + if (!_dottedStrokeStyle) + { + static constexpr D2D1_STROKE_STYLE_PROPERTIES props{ .dashStyle = D2D1_DASH_STYLE_CUSTOM }; + static constexpr FLOAT dashes[2]{ 1, 1 }; + THROW_IF_FAILED(p.d2dFactory->CreateStrokeStyle(&props, &dashes[0], 2, _dottedStrokeStyle.addressof())); + } appendHorizontalLine(r, p.s->font->underline, _dottedStrokeStyle.get(), r.underlineColor); } + else if (r.lines.test(GridLines::DashedUnderline)) + { + if (!_dashedStrokeStyle) + { + static constexpr D2D1_STROKE_STYLE_PROPERTIES props{ .dashStyle = D2D1_DASH_STYLE_CUSTOM }; + static constexpr FLOAT dashes[2]{ 2, 2 }; + THROW_IF_FAILED(p.d2dFactory->CreateStrokeStyle(&props, &dashes[0], 2, _dashedStrokeStyle.addressof())); + } + appendHorizontalLine(r, p.s->font->underline, _dashedStrokeStyle.get(), r.underlineColor); + } + else if (r.lines.test(GridLines::CurlyUnderline)) + { + appendCurlyLine(r); + } else if (r.lines.test(GridLines::DoubleUnderline)) { for (const auto pos : p.s->font->doubleUnderline) diff --git a/src/renderer/atlas/BackendD2D.h b/src/renderer/atlas/BackendD2D.h index e6993d60603..4206390ea52 100644 --- a/src/renderer/atlas/BackendD2D.h +++ b/src/renderer/atlas/BackendD2D.h @@ -3,9 +3,8 @@ #pragma once -#include - #include "Backend.h" +#include "BuiltinGlyphs.h" namespace Microsoft::Console::Render::Atlas { @@ -19,6 +18,10 @@ namespace Microsoft::Console::Render::Atlas ATLAS_ATTR_COLD void _handleSettingsUpdate(const RenderingPayload& p); void _drawBackground(const RenderingPayload& p); void _drawText(RenderingPayload& p); + ATLAS_ATTR_COLD f32 _drawBuiltinGlyphs(const RenderingPayload& p, const ShapedRow* row, const FontMapping& m, f32 baselineY, f32 baselineX); + void _prepareBuiltinGlyphRenderTarget(const RenderingPayload& p); + D2D1_RECT_U _prepareBuiltinGlyph(const RenderingPayload& p, char32_t ch, u32 off); + void _flushBuiltinGlyphs(); ATLAS_ATTR_COLD f32 _drawTextPrepareLineRendition(const RenderingPayload& p, const ShapedRow* row, f32 baselineY) const noexcept; ATLAS_ATTR_COLD void _drawTextResetLineRendition(const ShapedRow* row) const noexcept; ATLAS_ATTR_COLD f32r _getGlyphRunDesignBounds(const DWRITE_GLYPH_RUN& glyphRun, f32 baselineX, f32 baselineY); @@ -37,10 +40,18 @@ namespace Microsoft::Console::Render::Atlas wil::com_ptr _renderTarget; wil::com_ptr _renderTarget4; // Optional. Supported since Windows 10 14393. wil::com_ptr _dottedStrokeStyle; + wil::com_ptr _dashedStrokeStyle; wil::com_ptr _backgroundBitmap; wil::com_ptr _backgroundBrush; til::generation_t _backgroundBitmapGeneration; + wil::com_ptr _builtinGlyphsRenderTarget; + wil::com_ptr _builtinGlyphsBitmap; + wil::com_ptr _builtinGlyphBatch; + u32 _builtinGlyphsBitmapCellCountU = 0; + bool _builtinGlyphsRenderTargetActive = false; + bool _builtinGlyphsReady[BuiltinGlyphs::TotalCharCount]{}; + wil::com_ptr _cursorBitmap; til::size _cursorBitmapSize; // in columns/rows diff --git a/src/renderer/atlas/BackendD3D.cpp b/src/renderer/atlas/BackendD3D.cpp index fe07271c16a..d85d6e0365f 100644 --- a/src/renderer/atlas/BackendD3D.cpp +++ b/src/renderer/atlas/BackendD3D.cpp @@ -295,20 +295,20 @@ void BackendD3D::_updateFontDependents(const RenderingPayload& p) // baseline of curlyline is at the middle of singly underline. When there's // limited space to draw a curlyline, we apply a limit on the peak height. { - const auto cellHeight = static_cast(font.cellSize.y); - const auto duTop = static_cast(font.doubleUnderline[0].position); - const auto duBottom = static_cast(font.doubleUnderline[1].position); - const auto duHeight = static_cast(font.doubleUnderline[0].height); + const int cellHeight = font.cellSize.y; + const int duTop = font.doubleUnderline[0].position; + const int duBottom = font.doubleUnderline[1].position; + const int duHeight = font.doubleUnderline[0].height; // This gives it the same position and height as our double-underline. There's no particular reason for that, apart from // it being simple to implement and robust against more peculiar fonts with unusually large/small descenders, etc. - // We still need to ensure though that it doesn't clip out of the cellHeight at the bottom. - const auto height = std::max(3.0f, duBottom + duHeight - duTop); - const auto top = std::min(duTop, floorf(cellHeight - height - duHeight)); + // We still need to ensure though that it doesn't clip out of the cellHeight at the bottom, which is why `position` has a min(). + const auto height = std::max(3, duBottom + duHeight - duTop); + const auto position = std::min(duTop, cellHeight - height - duHeight); _curlyLineHalfHeight = height * 0.5f; - _curlyUnderline.position = gsl::narrow_cast(lrintf(top)); - _curlyUnderline.height = gsl::narrow_cast(lrintf(height)); + _curlyUnderline.position = gsl::narrow_cast(position); + _curlyUnderline.height = gsl::narrow_cast(height); } DWrite_GetRenderParams(p.dwriteFactory.get(), &_gamma, &_cleartypeEnhancedContrast, &_grayscaleEnhancedContrast, _textRenderingParams.put()); @@ -1509,7 +1509,19 @@ BackendD3D::AtlasGlyphEntry* BackendD3D::_drawBuiltinGlyph(const RenderingPayloa } else { - BuiltinGlyphs::DrawBuiltinGlyph(p.d2dFactory.get(), _d2dRenderTarget.get(), _brush.get(), r, glyphIndex); + // This code works in tandem with SHADING_TYPE_TEXT_BUILTIN_GLYPH in our pixel shader. + // Unless someone removed it, it should have a lengthy comment visually explaining + // what each of the 3 RGB components do. The short version is: + // R: stretch the checkerboard pattern (Shape_Filled050) horizontally + // G: invert the pixels + // B: overrides the above and fills it + static constexpr D2D1_COLOR_F shadeColorMap[] = { + { 1, 0, 0, 1 }, // Shape_Filled025 + { 0, 0, 0, 1 }, // Shape_Filled050 + { 1, 1, 0, 1 }, // Shape_Filled075 + { 1, 1, 1, 1 }, // Shape_Filled100 + }; + BuiltinGlyphs::DrawBuiltinGlyph(p.d2dFactory.get(), _d2dRenderTarget.get(), _brush.get(), shadeColorMap, r, glyphIndex); shadingType = ShadingType::TextBuiltinGlyph; } diff --git a/src/renderer/atlas/BuiltinGlyphs.cpp b/src/renderer/atlas/BuiltinGlyphs.cpp index c46bfc01fc0..92d87bc0b45 100644 --- a/src/renderer/atlas/BuiltinGlyphs.cpp +++ b/src/renderer/atlas/BuiltinGlyphs.cpp @@ -135,8 +135,6 @@ inline constexpr f32 Pos_Lut[][2] = { /* Pos_11_12 */ { 11.0f / 12.0f, 0.0f }, }; -static constexpr char32_t BoxDrawing_FirstChar = 0x2500; -static constexpr u32 BoxDrawing_CharCount = 0xA0; static constexpr Instruction BoxDrawing[BoxDrawing_CharCount][InstructionsPerGlyph] = { // U+2500 ─ BOX DRAWINGS LIGHT HORIZONTAL { @@ -964,8 +962,6 @@ static constexpr Instruction BoxDrawing[BoxDrawing_CharCount][InstructionsPerGly }, }; -static constexpr char32_t Powerline_FirstChar = 0xE0B0; -static constexpr u32 Powerline_CharCount = 0x10; static constexpr Instruction Powerline[Powerline_CharCount][InstructionsPerGlyph] = { // U+E0B0 Right triangle solid { @@ -1071,7 +1067,20 @@ static const Instruction* GetInstructions(char32_t codepoint) noexcept return nullptr; } -void BuiltinGlyphs::DrawBuiltinGlyph(ID2D1Factory* factory, ID2D1DeviceContext* renderTarget, ID2D1SolidColorBrush* brush, const D2D1_RECT_F& rect, char32_t codepoint) +i32 BuiltinGlyphs::GetBitmapCellIndex(char32_t codepoint) noexcept +{ + if (BoxDrawing_IsMapped(codepoint)) + { + return codepoint - BoxDrawing_FirstChar; + } + if (Powerline_IsMapped(codepoint)) + { + return codepoint - Powerline_FirstChar + BoxDrawing_CharCount; + } + return -1; +} + +void BuiltinGlyphs::DrawBuiltinGlyph(ID2D1Factory* factory, ID2D1DeviceContext* renderTarget, ID2D1SolidColorBrush* brush, const D2D1_COLOR_F (&shadeColorMap)[4], const D2D1_RECT_F& rect, char32_t codepoint) { renderTarget->PushAxisAlignedClip(&rect, D2D1_ANTIALIAS_MODE_ALIASED); const auto restoreD2D = wil::scope_exit([&]() { @@ -1122,15 +1131,18 @@ void BuiltinGlyphs::DrawBuiltinGlyph(ID2D1Factory* factory, ID2D1DeviceContext* const auto lineOffsetX = isHollowRect || isLineX ? lineWidthHalf : 0.0f; const auto lineOffsetY = isHollowRect || isLineY ? lineWidthHalf : 0.0f; - begX = roundf(begX - lineOffsetX) + lineOffsetX; - begY = roundf(begY - lineOffsetY) + lineOffsetY; - endX = roundf(endX + lineOffsetX) - lineOffsetX; - endY = roundf(endY + lineOffsetY) - lineOffsetY; - - const auto begXabs = begX + rectX; - const auto begYabs = begY + rectY; - const auto endXabs = endX + rectX; - const auto endYabs = endY + rectY; + // Direct2D draws strokes centered on the path. In order to make them pixel-perfect we need to round the + // coordinates to whole pixels, but offset by half the stroke width (= the radius of the stroke). + // + // All floats up to this point will be highly "consistent" between different `rect`s of identical size and + // different shapes, because the above calculations work with only a small set of constant floats. + // However, the addition of a potentially fractional begX/Y with a highly variable `rect` position is different. + // Rounding beg/endX/Y first ensures that we continue to get a consistent behavior between calls. + // This is particularly noticeable at smaller font sizes, where the line width is just a pixel or two. + const auto begXabs = rectX + roundf(begX - lineOffsetX) + lineOffsetX; + const auto begYabs = rectY + roundf(begY - lineOffsetY) + lineOffsetY; + const auto endXabs = rectX + roundf(endX + lineOffsetX) - lineOffsetX; + const auto endYabs = rectY + roundf(endY + lineOffsetY) - lineOffsetY; switch (shape) { @@ -1139,21 +1151,8 @@ void BuiltinGlyphs::DrawBuiltinGlyph(ID2D1Factory* factory, ID2D1DeviceContext* case Shape_Filled075: case Shape_Filled100: { - // This code works in tandem with SHADING_TYPE_TEXT_BUILTIN_GLYPH in our pixel shader. - // Unless someone removed it, it should have a lengthy comment visually explaining - // what each of the 3 RGB components do. The short version is: - // R: stretch the checkerboard pattern (Shape_Filled050) horizontally - // G: invert the pixels - // B: overrides the above and fills it - static constexpr D2D1_COLOR_F colors[] = { - { 1, 0, 0, 1 }, // Shape_Filled025 - { 0, 0, 0, 1 }, // Shape_Filled050 - { 1, 1, 0, 1 }, // Shape_Filled075 - { 1, 1, 1, 1 }, // Shape_Filled100 - }; - const auto brushColor = brush->GetColor(); - brush->SetColor(&colors[shape]); + brush->SetColor(&shadeColorMap[shape]); const D2D1_RECT_F r{ begXabs, begYabs, endXabs, endYabs }; renderTarget->FillRectangle(&r, brush); @@ -1183,13 +1182,13 @@ void BuiltinGlyphs::DrawBuiltinGlyph(ID2D1Factory* factory, ID2D1DeviceContext* } case Shape_FilledEllipsis: { - const D2D1_ELLIPSE e{ { begXabs, begYabs }, endX, endY }; + const D2D1_ELLIPSE e{ { rectX + begX, rectY + begY }, endX, endY }; renderTarget->FillEllipse(&e, brush); break; } case Shape_EmptyEllipsis: { - const D2D1_ELLIPSE e{ { begXabs, begYabs }, endX, endY }; + const D2D1_ELLIPSE e{ { rectX + begX, rectY + begY }, endX, endY }; renderTarget->DrawEllipse(&e, brush, lineWidth, nullptr); break; } diff --git a/src/renderer/atlas/BuiltinGlyphs.h b/src/renderer/atlas/BuiltinGlyphs.h index f399653893c..b6cce3a3974 100644 --- a/src/renderer/atlas/BuiltinGlyphs.h +++ b/src/renderer/atlas/BuiltinGlyphs.h @@ -8,7 +8,17 @@ namespace Microsoft::Console::Render::Atlas::BuiltinGlyphs { bool IsBuiltinGlyph(char32_t codepoint) noexcept; - void DrawBuiltinGlyph(ID2D1Factory* factory, ID2D1DeviceContext* renderTarget, ID2D1SolidColorBrush* brush, const D2D1_RECT_F& rect, char32_t codepoint); + void DrawBuiltinGlyph(ID2D1Factory* factory, ID2D1DeviceContext* renderTarget, ID2D1SolidColorBrush* brush, const D2D1_COLOR_F (&shadeColorMap)[4], const D2D1_RECT_F& rect, char32_t codepoint); + + inline constexpr char32_t BoxDrawing_FirstChar = 0x2500; + inline constexpr u32 BoxDrawing_CharCount = 0xA0; + + inline constexpr char32_t Powerline_FirstChar = 0xE0B0; + inline constexpr u32 Powerline_CharCount = 0x10; + + inline constexpr u32 TotalCharCount = BoxDrawing_CharCount + Powerline_CharCount; + + i32 GetBitmapCellIndex(char32_t codepoint) noexcept; // This is just an extra. It's not actually implemented as part of BuiltinGlyphs.cpp. constexpr bool IsSoftFontChar(char32_t ch) noexcept diff --git a/src/tools/RenderingTests/main.cpp b/src/tools/RenderingTests/main.cpp index 45dfcf66ea7..2d0ef8612a4 100644 --- a/src/tools/RenderingTests/main.cpp +++ b/src/tools/RenderingTests/main.cpp @@ -108,15 +108,15 @@ static void printfUTF16(_In_z_ _Printf_format_string_ wchar_t const* const forma static void wait() { - printUTF16(L"\x1B[9999;1HPress any key to continue..."); + printUTF16(L"\x1b[9999;1HPress any key to continue..."); _getch(); } static void clear() { printUTF16( - L"\x1B[H" // move cursor to 0,0 - L"\x1B[2J" // clear screen + L"\x1b[H" // move cursor to 0,0 + L"\x1b[2J" // clear screen ); } @@ -166,7 +166,7 @@ int main() for (const auto& t : consoleAttributeTests) { const auto length = static_cast(wcslen(t.text)); - printfUTF16(L"\x1B[%d;5H%s", row + 1, t.text); + printfUTF16(L"\x1b[%d;5H%s", row + 1, t.text); WORD attributes[32]; std::fill_n(&attributes[0], length, static_cast(FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED | t.attribute)); @@ -190,16 +190,16 @@ int main() { L"overlined", 53 }, }; - printfUTF16(L"\x1B[3;39HANSI escape SGR:"); + printfUTF16(L"\x1b[3;39HANSI escape SGR:"); int row = 5; for (const auto& t : basicSGR) { - printfUTF16(L"\x1B[%d;39H\x1b[%dm%s\x1b[m", row, t.attribute, t.text); + printfUTF16(L"\x1b[%d;39H\x1b[%dm%s\x1b[m", row, t.attribute, t.text); row += 2; } - printfUTF16(L"\x1B[%d;39H\x1b]8;;https://example.com\x1b\\hyperlink\x1b]8;;\x1b\\", row); + printfUTF16(L"\x1b[%d;39H\x1b]8;;https://example.com\x1b\\hyperlink\x1b]8;;\x1b\\", row); } { @@ -211,18 +211,18 @@ int main() { L"dashed", 5 }, }; - printfUTF16(L"\x1B[3;63HStyled Underlines:"); + printfUTF16(L"\x1b[3;63HStyled Underlines:"); int row = 5; for (const auto& t : styledUnderlines) { - printfUTF16(L"\x1B[%d;63H\x1b[4:%dm", row, t.attribute); + printfUTF16(L"\x1b[%d;63H\x1b[4:%dm", row, t.attribute); const auto len = wcslen(t.text); for (size_t i = 0; i < len; ++i) { const auto color = colorbrewer::pastel1[i % std::size(colorbrewer::pastel1)]; - printfUTF16(L"\x1B[58:2::%d:%d:%dm%c", (color >> 16) & 0xff, (color >> 8) & 0xff, color & 0xff, t.text[i]); + printfUTF16(L"\x1b[58:2::%d:%d:%dm%c", (color >> 16) & 0xff, (color >> 8) & 0xff, color & 0xff, t.text[i]); } printfUTF16(L"\x1b[m"); @@ -236,19 +236,19 @@ int main() { printUTF16( - L"\x1B[3;5HDECDWL Double Width \U0001FAE0 \x1B[45;92mA\u0353\u0353\x1B[m B\u036F\u036F" - L"\x1B[4;3H\x1b#6DECDWL Double Width \U0001FAE0 \x1B[45;92mA\u0353\u0353\x1B[m B\u036F\u036F" - L"\x1B[7;5HDECDHL Double Height \U0001F952\U0001F6C1 A\u0353\u0353 \x1B[45;92mB\u036F\u036F\x1B[m \x1B[45;92mX\u0353\u0353\x1B[m Y\u036F\u036F" - L"\x1B[8;3H\x1b#3DECDHL Double Height Top \U0001F952 A\u0353\u0353 \x1B[45;92mB\u036F\u036F\x1B[m" - L"\x1B[9;3H\x1b#4DECDHL Double Height Bottom \U0001F6C1 \x1B[45;92mX\u0353\u0353\x1B[m Y\u036F\u036F" - L"\x1B[13;5H\x1b]8;;https://example.com\x1b\\DECDxL\x1b]8;;\x1b\\ <\x1B[45;92m!\x1B[m-- \x1B[3mitalic\x1b[m \x1b[4munderline\x1b[m \x1b[7mreverse\x1b[m" - L"\x1B[15;5H\x1b]8;;https://example.com\x1b\\DECDxL\x1b]8;;\x1b\\ <\x1B[45;92m!\x1B[m-- \x1b[9mstrikethrough\x1b[m \x1b[21mdouble underline\x1b[m \x1b[53moverlined\x1b[m" - L"\x1B[17;3H\x1b#6\x1b]8;;https://vt100.net/docs/vt510-rm/DECDWL.html\x1b\\DECDWL\x1b]8;;\x1b\\ <\x1B[45;92m!\x1B[m-- \x1B[3mitalic\x1b[m \x1b[4munderline\x1b[m \x1b[7mreverse\x1b[m" - L"\x1B[19;3H\x1b#6\x1b]8;;https://vt100.net/docs/vt510-rm/DECDWL.html\x1b\\DECDWL\x1b]8;;\x1b\\ <\x1B[45;92m!\x1B[m-- \x1b[9mstrikethrough\x1b[m \x1b[21mdouble underline\x1b[m \x1b[53moverlined\x1b[m" - L"\x1B[21;3H\x1b#3\x1b]8;;https://vt100.net/docs/vt510-rm/DECDHL.html\x1b\\DECDHL\x1b]8;;\x1b\\ <\x1B[45;92m!\x1B[m-- \x1B[3mitalic\x1b[m \x1b[4munderline\x1b[m \x1b[7mreverse\x1b[m" - L"\x1B[22;3H\x1b#4\x1b]8;;https://vt100.net/docs/vt510-rm/DECDHL.html\x1b\\DECDHL\x1b]8;;\x1b\\ <\x1B[45;92m!\x1B[m-- \x1B[3mitalic\x1b[m \x1b[4munderline\x1b[m \x1b[7mreverse\x1b[m" - L"\x1B[24;3H\x1b#3\x1b]8;;https://vt100.net/docs/vt510-rm/DECDHL.html\x1b\\DECDHL\x1b]8;;\x1b\\ <\x1B[45;92m!\x1B[m-- \x1b[9mstrikethrough\x1b[m \x1b[21mdouble underline\x1b[m \x1b[53moverlined\x1b[m" - L"\x1B[25;3H\x1b#4\x1b]8;;https://vt100.net/docs/vt510-rm/DECDHL.html\x1b\\DECDHL\x1b]8;;\x1b\\ <\x1B[45;92m!\x1B[m-- \x1b[9mstrikethrough\x1b[m \x1b[21mdouble underline\x1b[m \x1b[53moverlined\x1b[m"); + L"\x1b[3;5HDECDWL Double Width \U0001FAE0 \x1b[45;92mA\u0353\u0353\x1b[m B\u036F\u036F" + L"\x1b[4;3H\x1b#6DECDWL Double Width \U0001FAE0 \x1b[45;92mA\u0353\u0353\x1b[m B\u036F\u036F" + L"\x1b[7;5HDECDHL Double Height \U0001F952\U0001F6C1 A\u0353\u0353 \x1b[45;92mB\u036F\u036F\x1b[m \x1b[45;92mX\u0353\u0353\x1b[m Y\u036F\u036F" + L"\x1b[8;3H\x1b#3DECDHL Double Height Top \U0001F952 A\u0353\u0353 \x1b[45;92mB\u036F\u036F\x1b[m" + L"\x1b[9;3H\x1b#4DECDHL Double Height Bottom \U0001F6C1 \x1b[45;92mX\u0353\u0353\x1b[m Y\u036F\u036F" + L"\x1b[12;5H\x1b]8;;https://example.com\x1b\\DECDxL\x1b]8;;\x1b\\ <\x1b[45;92m!\x1b[m-- \x1b[3;4:3;58:2::255:0:0mita\x1b[58:2::0:255:0mlic\x1b[m \x1b[4munderline\x1b[m \x1b[7mreverse\x1b[m" + L"\x1b[14;5H\x1b]8;;https://example.com\x1b\\DECDxL\x1b]8;;\x1b\\ <\x1b[45;92m!\x1b[m-- \x1b[9mstrikethrough\x1b[m \x1b[21mdouble underline\x1b[m \x1b[53moverlined\x1b[m" + L"\x1b[16;3H\x1b#6\x1b]8;;https://vt100.net/docs/vt510-rm/DECDWL.html\x1b\\DECDWL\x1b]8;;\x1b\\ <\x1b[45;92m!\x1b[m-- \x1b[3;4:3;58:2::255:0:0mita\x1b[58:2::0:255:0mlic\x1b[m \x1b[4munderline\x1b[m \x1b[7mreverse\x1b[m" + L"\x1b[18;3H\x1b#6\x1b]8;;https://vt100.net/docs/vt510-rm/DECDWL.html\x1b\\DECDWL\x1b]8;;\x1b\\ <\x1b[45;92m!\x1b[m-- \x1b[9mstrikethrough\x1b[m \x1b[21mdouble underline\x1b[m \x1b[53moverlined\x1b[m" + L"\x1b[20;3H\x1b#3\x1b]8;;https://vt100.net/docs/vt510-rm/DECDHL.html\x1b\\DECDHL\x1b]8;;\x1b\\ <\x1b[45;92m!\x1b[m-- \x1b[3;4:3;58:2::255:0:0mita\x1b[58:2::0:255:0mlic\x1b[m \x1b[4munderline\x1b[m \x1b[7mreverse\x1b[m" + L"\x1b[21;3H\x1b#4\x1b]8;;https://vt100.net/docs/vt510-rm/DECDHL.html\x1b\\DECDHL\x1b]8;;\x1b\\ <\x1b[45;92m!\x1b[m-- \x1b[3;4:3;58:2::255:0:0mita\x1b[58:2::0:255:0mlic\x1b[m \x1b[4munderline\x1b[m \x1b[7mreverse\x1b[m" + L"\x1b[23;3H\x1b#3\x1b]8;;https://vt100.net/docs/vt510-rm/DECDHL.html\x1b\\DECDHL\x1b]8;;\x1b\\ <\x1b[45;92m!\x1b[m-- \x1b[9mstrikethrough\x1b[m \x1b[21mdouble underline\x1b[m \x1b[53moverlined\x1b[m" + L"\x1b[24;3H\x1b#4\x1b]8;;https://vt100.net/docs/vt510-rm/DECDHL.html\x1b\\DECDHL\x1b]8;;\x1b\\ <\x1b[45;92m!\x1b[m-- \x1b[9mstrikethrough\x1b[m \x1b[21mdouble underline\x1b[m \x1b[53moverlined\x1b[m"); static constexpr WORD attributes[]{ FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED | COMMON_LVB_GRID_HORIZONTAL, @@ -264,7 +264,7 @@ int main() DWORD numberOfAttrsWritten; DWORD offset = 0; - for (const auto r : { 12, 14, 16, 18, 20, 21, 23, 24 }) + for (const auto r : { 11, 13, 15, 17, 19, 20, 22, 23 }) { COORD coord; coord.X = r > 14 ? 2 : 4; @@ -338,14 +338,14 @@ int main() #define DRCS_SEQUENCE L"\x1b( @#\x1b(A" printUTF16( - L"\x1B[3;5HDECDLD and DRCS test - it should show \"WT\" in a single cell" - L"\x1B[5;5HRegular: " DRCS_SEQUENCE L"" - L"\x1B[7;3H\x1b#6DECDWL: " DRCS_SEQUENCE L"" - L"\x1B[9;3H\x1b#3DECDHL: " DRCS_SEQUENCE L"" - L"\x1B[10;3H\x1b#4DECDHL: " DRCS_SEQUENCE L"" + L"\x1b[3;5HDECDLD and DRCS test - it should show \"WT\" in a single cell" + L"\x1b[5;5HRegular: " DRCS_SEQUENCE L"" + L"\x1b[7;3H\x1b#6DECDWL: " DRCS_SEQUENCE L"" + L"\x1b[9;3H\x1b#3DECDHL: " DRCS_SEQUENCE L"" + L"\x1b[10;3H\x1b#4DECDHL: " DRCS_SEQUENCE L"" // We map soft fonts into the private use area starting at U+EF20. This test ensures // that we correctly map actual fallback glyphs mixed into the DRCS glyphs. - L"\x1B[12;5HUnicode Fallback: \uE000\uE001" DRCS_SEQUENCE L"\uE003\uE004"); + L"\x1b[12;5HUnicode Fallback: \uE000\uE001" DRCS_SEQUENCE L"\uE003\uE004"); #undef DRCS_SEQUENCE wait(); From 26cc5da7f1dd860dab8bfe8595853dd674d4c463 Mon Sep 17 00:00:00 2001 From: Windows Console Service Bot <14666831+consvc@users.noreply.github.com> Date: Fri, 17 May 2024 12:42:09 -0500 Subject: [PATCH 2/4] Localization Updates - main - associated with #17259 (#17272) --- .../TerminalSettingsEditor/Resources/de-DE/Resources.resw | 5 ++++- .../TerminalSettingsEditor/Resources/es-ES/Resources.resw | 5 ++++- .../TerminalSettingsEditor/Resources/fr-FR/Resources.resw | 5 ++++- .../TerminalSettingsEditor/Resources/it-IT/Resources.resw | 5 ++++- .../TerminalSettingsEditor/Resources/ja-JP/Resources.resw | 5 ++++- .../TerminalSettingsEditor/Resources/ko-KR/Resources.resw | 5 ++++- .../TerminalSettingsEditor/Resources/pt-BR/Resources.resw | 5 ++++- .../TerminalSettingsEditor/Resources/qps-ploc/Resources.resw | 5 ++++- .../Resources/qps-ploca/Resources.resw | 5 ++++- .../Resources/qps-plocm/Resources.resw | 5 ++++- .../TerminalSettingsEditor/Resources/ru-RU/Resources.resw | 5 ++++- .../TerminalSettingsEditor/Resources/zh-CN/Resources.resw | 5 ++++- .../TerminalSettingsEditor/Resources/zh-TW/Resources.resw | 5 ++++- 13 files changed, 52 insertions(+), 13 deletions(-) diff --git a/src/cascadia/TerminalSettingsEditor/Resources/de-DE/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/de-DE/Resources.resw index 27e9ace6da2..3cd573f25f4 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/de-DE/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/de-DE/Resources.resw @@ -895,6 +895,9 @@ Schriftart Name for a control to select the font for text in the app. + + Sie können mehrere Schriftarten verwenden, indem Sie sie durch ein ASCII-Komma voneinander trennen. + Schriftgrad Header for a control to determine the size of the text in the app. @@ -928,7 +931,7 @@ The main label of a toggle. When enabled, certain characters (glyphs) are replaced with better looking ones. - Wenn diese Option aktiviert ist, zeichnet das Terminal benutzerdefinierte Glyphen für Blockelement- und Feldzeichnungszeichen, anstatt die Schriftart zu verwenden. Dieses Feature funktioniert nur, wenn GPU-Beschleunigung verfügbar ist. + Wenn diese Option aktiviert ist, zeichnet das Terminal benutzerdefinierte Glyphen für Blockelement- und Feldzeichnungszeichen, anstatt die Schriftart zu verwenden. Dieses Feature ist nicht verfügbar, wenn Direct2D als Grafik-API verwendet wird. A longer description of the "Profile_EnableBuiltinGlyphs" toggle. "glyphs", "block element" and "box drawing characters" are technical terms from the Unicode specification. diff --git a/src/cascadia/TerminalSettingsEditor/Resources/es-ES/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/es-ES/Resources.resw index 61ef4151625..49806cb4fb1 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/es-ES/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/es-ES/Resources.resw @@ -895,6 +895,9 @@ Tipo de fuente Name for a control to select the font for text in the app. + + Puede usar varias fuentes separándolas con una coma ASCII. + Tamaño de la fuente Header for a control to determine the size of the text in the app. @@ -928,7 +931,7 @@ The main label of a toggle. When enabled, certain characters (glyphs) are replaced with better looking ones. - Cuando está habilitado, el terminal dibuja glifos personalizados para los caracteres de dibujo de elementos de bloque y cuadros, en lugar de usar la fuente. Esta característica solo funciona cuando la aceleración de GPU está disponible. + Cuando está habilitado, el terminal dibuja glifos personalizados para los caracteres de dibujo de elementos de bloque y cuadros, en lugar de usar la fuente. Esta característica no está disponible cuando se usa Direct2D como API de gráficos. A longer description of the "Profile_EnableBuiltinGlyphs" toggle. "glyphs", "block element" and "box drawing characters" are technical terms from the Unicode specification. diff --git a/src/cascadia/TerminalSettingsEditor/Resources/fr-FR/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/fr-FR/Resources.resw index 7f2a42f5f38..1c551a0699f 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/fr-FR/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/fr-FR/Resources.resw @@ -895,6 +895,9 @@ Type de police Name for a control to select the font for text in the app. + + Vous pouvez utiliser plusieurs polices en les séparant par une virgule ASCII. + Taille de police Header for a control to determine the size of the text in the app. @@ -928,7 +931,7 @@ The main label of a toggle. When enabled, certain characters (glyphs) are replaced with better looking ones. - Lorsque cette option est activée, le terminal dessine des glyphes personnalisés pour les éléments de bloc et les caractères de dessin de boîte au lieu d’utiliser la police. Cette fonctionnalité ne fonctionne que lorsque l’accélération GPU est disponible. + Lorsque cette option est activée, le terminal dessine des glyphes personnalisés pour les éléments de bloc et les caractères de dessin de boîte au lieu d’utiliser la police. Cette fonctionnalité n’est pas disponible lors de l’utilisation de Direct2D comme API Graphics. A longer description of the "Profile_EnableBuiltinGlyphs" toggle. "glyphs", "block element" and "box drawing characters" are technical terms from the Unicode specification. diff --git a/src/cascadia/TerminalSettingsEditor/Resources/it-IT/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/it-IT/Resources.resw index 1b2550d1d61..ca77707ca2f 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/it-IT/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/it-IT/Resources.resw @@ -895,6 +895,9 @@ Tipo di carattere Name for a control to select the font for text in the app. + + Puoi usare più tipi di carattere separandoli con una virgola ASCII. + Dimensioni del carattere Header for a control to determine the size of the text in the app. @@ -928,7 +931,7 @@ The main label of a toggle. When enabled, certain characters (glyphs) are replaced with better looking ones. - Se abilitata, il terminale disegna glifi personalizzati per i caratteri di disegno a blocchi di elementi e caselle anziché usare il tipo di carattere. Questa funzionalità funziona solo quando è disponibile l'accelerazione GPU. + Se abilitata, il terminale disegna glifi personalizzati per i caratteri di disegno a blocchi di elementi e caselle anziché usare il tipo di carattere. Questa funzionalità non è disponibile quando si usa Direct2D come API grafica. A longer description of the "Profile_EnableBuiltinGlyphs" toggle. "glyphs", "block element" and "box drawing characters" are technical terms from the Unicode specification. diff --git a/src/cascadia/TerminalSettingsEditor/Resources/ja-JP/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/ja-JP/Resources.resw index 3ba41206daa..eca2e0bcf96 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/ja-JP/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/ja-JP/Resources.resw @@ -895,6 +895,9 @@ フォント フェイス Name for a control to select the font for text in the app. + + 複数のフォントを使用するには、複数のフォントを ASCII コンマで区切ります。 + フォント サイズ Header for a control to determine the size of the text in the app. @@ -928,7 +931,7 @@ The main label of a toggle. When enabled, certain characters (glyphs) are replaced with better looking ones. - 有効にすると、ターミナルは、フォントを使用する代わりに、ブロック要素とボックス描画文字のカスタム グリフを描画します。この機能は、GPU アクセラレーションが使用可能な場合にのみ機能します。 + 有効にすると、ターミナルは、フォントを使用する代わりに、ブロック要素とボックス描画文字のカスタム グリフを描画します。グラフィックス API として Direct2D を使用している場合、この機能は使用できません。 A longer description of the "Profile_EnableBuiltinGlyphs" toggle. "glyphs", "block element" and "box drawing characters" are technical terms from the Unicode specification. diff --git a/src/cascadia/TerminalSettingsEditor/Resources/ko-KR/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/ko-KR/Resources.resw index 4e022bd43b8..976717c8677 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/ko-KR/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/ko-KR/Resources.resw @@ -895,6 +895,9 @@ 글꼴 Name for a control to select the font for text in the app. + + 여러 글꼴을 ASCII 쉼표로 구분하여 사용할 수 있습니다. + 글꼴 크기 Header for a control to determine the size of the text in the app. @@ -928,7 +931,7 @@ The main label of a toggle. When enabled, certain characters (glyphs) are replaced with better looking ones. - 사용하도록 설정하면 터미널은 글꼴을 사용하는 대신 블록 요소 및 상자 그리기 문자에 대한 사용자 지정 문자 모양을 그립니다. 이 기능은 GPU 가속을 사용할 수 있는 경우에만 작동합니다. + 사용하도록 설정하면 터미널은 글꼴을 사용하는 대신 블록 요소 및 상자 그리기 문자에 대한 사용자 지정 문자 모양을 그립니다. Direct2D를 그래픽 API로 사용할 때는 이 기능을 사용할 수 없습니다. A longer description of the "Profile_EnableBuiltinGlyphs" toggle. "glyphs", "block element" and "box drawing characters" are technical terms from the Unicode specification. diff --git a/src/cascadia/TerminalSettingsEditor/Resources/pt-BR/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/pt-BR/Resources.resw index 8ac46dede93..ecdcef31cfc 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/pt-BR/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/pt-BR/Resources.resw @@ -895,6 +895,9 @@ Tipo de fonte Name for a control to select the font for text in the app. + + Pode utilizar vários tipos de letra separando-os com uma vírgula ASCII. + Tamanho da fonte Header for a control to determine the size of the text in the app. @@ -928,7 +931,7 @@ The main label of a toggle. When enabled, certain characters (glyphs) are replaced with better looking ones. - Quando habilitado, o terminal desenha glifos personalizados para caracteres de desenho de elemento de bloco e caixa em vez de usar a fonte. Esse recurso só funciona quando a Aceleração de GPU está disponível. + Quando habilitado, o terminal desenha glifos personalizados para caracteres de desenho de elemento de bloco e caixa em vez de usar a fonte. Esse recurso não está disponível ao usar o Direct2D como a API de Gráficos. A longer description of the "Profile_EnableBuiltinGlyphs" toggle. "glyphs", "block element" and "box drawing characters" are technical terms from the Unicode specification. diff --git a/src/cascadia/TerminalSettingsEditor/Resources/qps-ploc/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/qps-ploc/Resources.resw index a9e220a076c..666fff836bb 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/qps-ploc/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/qps-ploc/Resources.resw @@ -895,6 +895,9 @@ ₣øñŧ ƒдсέ !!! Name for a control to select the font for text in the app. + + Ýоύ ĉąи ύŝè мύŀŧΐρļĕ ƒőйτš ъý ѕéραгдŧĭʼnğ ţнэm щїţђ áп ∆ŚČĨĨ çőмmа. !!! !!! !!! !!! !!! !!! ! + ₣ŏňτ şίźε !!! Header for a control to determine the size of the text in the app. @@ -928,7 +931,7 @@ The main label of a toggle. When enabled, certain characters (glyphs) are replaced with better looking ones. - Ẅħĕň еņªъℓёð, тћз τēгмïπдℓ ðѓáшş çύšτом ģļўφħѕ ƒǿя ъĺôсĸ éℓêm℮ήŧ àñð ъο× ðгдωΐπĝ čћдŗąςŧέѓś ìπŝţėãđ ŏƒ ũŝіⁿğ ťћę ƒбηŧ. Ţћιş ƒзаτџŕè οʼnĺŷ ωóгκŝ ẁĥéŋ ĢΡŬ Àςĉеŀèѓдτισñ ĩѕ ǻνãîļåвłę. !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !! + Ẅħĕň еņªъℓёð, тћз τēгмïπдℓ ðѓáшş çύšτом ģļўφħѕ ƒǿя ъĺôсĸ éℓêm℮ήŧ àñð ъο× ðгдωΐπĝ čћдŗąςŧέѓś ìπŝţėãđ ŏƒ ũŝіⁿğ ťћę ƒбηŧ. Ţћιş ƒзаτџŕè ίš úπâνάίļǻвĺз ẅнέи μşιńĝ Ďΐґеčт2Ď άѕ тħë Ĝяãрнĩςŝ ÃРÌ. !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !! A longer description of the "Profile_EnableBuiltinGlyphs" toggle. "glyphs", "block element" and "box drawing characters" are technical terms from the Unicode specification. diff --git a/src/cascadia/TerminalSettingsEditor/Resources/qps-ploca/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/qps-ploca/Resources.resw index a9e220a076c..666fff836bb 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/qps-ploca/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/qps-ploca/Resources.resw @@ -895,6 +895,9 @@ ₣øñŧ ƒдсέ !!! Name for a control to select the font for text in the app. + + Ýоύ ĉąи ύŝè мύŀŧΐρļĕ ƒőйτš ъý ѕéραгдŧĭʼnğ ţнэm щїţђ áп ∆ŚČĨĨ çőмmа. !!! !!! !!! !!! !!! !!! ! + ₣ŏňτ şίźε !!! Header for a control to determine the size of the text in the app. @@ -928,7 +931,7 @@ The main label of a toggle. When enabled, certain characters (glyphs) are replaced with better looking ones. - Ẅħĕň еņªъℓёð, тћз τēгмïπдℓ ðѓáшş çύšτом ģļўφħѕ ƒǿя ъĺôсĸ éℓêm℮ήŧ àñð ъο× ðгдωΐπĝ čћдŗąςŧέѓś ìπŝţėãđ ŏƒ ũŝіⁿğ ťћę ƒбηŧ. Ţћιş ƒзаτџŕè οʼnĺŷ ωóгκŝ ẁĥéŋ ĢΡŬ Àςĉеŀèѓдτισñ ĩѕ ǻνãîļåвłę. !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !! + Ẅħĕň еņªъℓёð, тћз τēгмïπдℓ ðѓáшş çύšτом ģļўφħѕ ƒǿя ъĺôсĸ éℓêm℮ήŧ àñð ъο× ðгдωΐπĝ čћдŗąςŧέѓś ìπŝţėãđ ŏƒ ũŝіⁿğ ťћę ƒбηŧ. Ţћιş ƒзаτџŕè ίš úπâνάίļǻвĺз ẅнέи μşιńĝ Ďΐґеčт2Ď άѕ тħë Ĝяãрнĩςŝ ÃРÌ. !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !! A longer description of the "Profile_EnableBuiltinGlyphs" toggle. "glyphs", "block element" and "box drawing characters" are technical terms from the Unicode specification. diff --git a/src/cascadia/TerminalSettingsEditor/Resources/qps-plocm/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/qps-plocm/Resources.resw index a9e220a076c..666fff836bb 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/qps-plocm/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/qps-plocm/Resources.resw @@ -895,6 +895,9 @@ ₣øñŧ ƒдсέ !!! Name for a control to select the font for text in the app. + + Ýоύ ĉąи ύŝè мύŀŧΐρļĕ ƒőйτš ъý ѕéραгдŧĭʼnğ ţнэm щїţђ áп ∆ŚČĨĨ çőмmа. !!! !!! !!! !!! !!! !!! ! + ₣ŏňτ şίźε !!! Header for a control to determine the size of the text in the app. @@ -928,7 +931,7 @@ The main label of a toggle. When enabled, certain characters (glyphs) are replaced with better looking ones. - Ẅħĕň еņªъℓёð, тћз τēгмïπдℓ ðѓáшş çύšτом ģļўφħѕ ƒǿя ъĺôсĸ éℓêm℮ήŧ àñð ъο× ðгдωΐπĝ čћдŗąςŧέѓś ìπŝţėãđ ŏƒ ũŝіⁿğ ťћę ƒбηŧ. Ţћιş ƒзаτџŕè οʼnĺŷ ωóгκŝ ẁĥéŋ ĢΡŬ Àςĉеŀèѓдτισñ ĩѕ ǻνãîļåвłę. !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !! + Ẅħĕň еņªъℓёð, тћз τēгмïπдℓ ðѓáшş çύšτом ģļўφħѕ ƒǿя ъĺôсĸ éℓêm℮ήŧ àñð ъο× ðгдωΐπĝ čћдŗąςŧέѓś ìπŝţėãđ ŏƒ ũŝіⁿğ ťћę ƒбηŧ. Ţћιş ƒзаτџŕè ίš úπâνάίļǻвĺз ẅнέи μşιńĝ Ďΐґеčт2Ď άѕ тħë Ĝяãрнĩςŝ ÃРÌ. !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !! A longer description of the "Profile_EnableBuiltinGlyphs" toggle. "glyphs", "block element" and "box drawing characters" are technical terms from the Unicode specification. diff --git a/src/cascadia/TerminalSettingsEditor/Resources/ru-RU/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/ru-RU/Resources.resw index 592a519ebd5..2ff2287c04e 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/ru-RU/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/ru-RU/Resources.resw @@ -895,6 +895,9 @@ Начертание шрифта Name for a control to select the font for text in the app. + + Вы можете использовать несколько шрифтов, разделив их запятой ASCII. + Размер шрифта Header for a control to determine the size of the text in the app. @@ -928,7 +931,7 @@ The main label of a toggle. When enabled, certain characters (glyphs) are replaced with better looking ones. - Если этот параметр включен, терминал рисует собственные глифы для элементов блока и символов рисования прямоугольников вместо использования шрифта. Эта функция работает только тогда, когда доступно ускорение графического процессора. + Если этот параметр включен, терминал рисует собственные глифы для элементов блока и символов рисования прямоугольников вместо использования шрифта. Эта функция недоступна при использовании Direct2D в качестве API графики. A longer description of the "Profile_EnableBuiltinGlyphs" toggle. "glyphs", "block element" and "box drawing characters" are technical terms from the Unicode specification. diff --git a/src/cascadia/TerminalSettingsEditor/Resources/zh-CN/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/zh-CN/Resources.resw index 6461b722902..b70e55b6996 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/zh-CN/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/zh-CN/Resources.resw @@ -895,6 +895,9 @@ 字体 Name for a control to select the font for text in the app. + + 通过使用 ASCII 逗号分隔多个字体,可以使用这些字体。 + 字号 Header for a control to determine the size of the text in the app. @@ -928,7 +931,7 @@ The main label of a toggle. When enabled, certain characters (glyphs) are replaced with better looking ones. - 启用后,终端将为块元素和制表符绘制自定义标志符号,而不是使用字体。仅当 GPU 加速可用时,此功能才有效。 + 启用后,终端会使用自定义字形来绘制块元素和框线绘图字符,而不是使用字体。使用 Direct2D 作为图形 API 时,此功能不可用。 A longer description of the "Profile_EnableBuiltinGlyphs" toggle. "glyphs", "block element" and "box drawing characters" are technical terms from the Unicode specification. diff --git a/src/cascadia/TerminalSettingsEditor/Resources/zh-TW/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/zh-TW/Resources.resw index f951dbc2d9e..2501d9d7be4 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/zh-TW/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/zh-TW/Resources.resw @@ -895,6 +895,9 @@ 字體 Name for a control to select the font for text in the app. + + 您可以使用 ASCII 逗號分隔多個字型。 + 字型大小 Header for a control to determine the size of the text in the app. @@ -928,7 +931,7 @@ The main label of a toggle. When enabled, certain characters (glyphs) are replaced with better looking ones. - 啟用時,終端機會為區塊元素和方塊繪圖字元繪製自訂字符,而非使用字型。此功能僅在可使用 GPU 加速時作用。 + 啟用時,終端機會為區塊元素和方塊繪圖字元繪製自訂字符,而非使用字型。此功能無法在使用 Direct2D 做為圖形 API 時使用。 A longer description of the "Profile_EnableBuiltinGlyphs" toggle. "glyphs", "block element" and "box drawing characters" are technical terms from the Unicode specification. From 097a2c11362514ea4154576dd2afac14aac3152f Mon Sep 17 00:00:00 2001 From: "Dustin L. Howett" Date: Fri, 17 May 2024 15:46:52 -0500 Subject: [PATCH 3/4] Revert Canary to supporting 10.0.19041 (Windows 10 Vb+) (#17284) The 2024.04D servicing update to Windows 10 added support for `appLicensing`. --- src/cascadia/CascadiaPackage/Package-Can.appxmanifest | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/cascadia/CascadiaPackage/Package-Can.appxmanifest b/src/cascadia/CascadiaPackage/Package-Can.appxmanifest index 164ce0bca43..72b0a648789 100644 --- a/src/cascadia/CascadiaPackage/Package-Can.appxmanifest +++ b/src/cascadia/CascadiaPackage/Package-Can.appxmanifest @@ -38,9 +38,7 @@ - - - + From 4a243f044572146e18e0051badb1b5b3f3c28ac8 Mon Sep 17 00:00:00 2001 From: James Holderness Date: Fri, 17 May 2024 22:49:23 +0100 Subject: [PATCH 4/4] Add support for VT paging operations (#16615) This PR adds support for multiples pages in the VT architecture, along with new operations for moving between those pages: `NP` (Next Page), `PP` (Preceding Page), `PPA` (Page Position Absolute), `PPR` (Page Position Relative), and `PPB` (Page Position Back). There's also a new mode, `DECPCCM` (Page Cursor Coupling Mode), which determines whether or not the active page is also the visible page, and a new query sequence, `DECRQDE` (Request Displayed Extent), which can be used to query the visible page. ## References and Relevant Issues When combined with `DECCRA` (Copy Rectangular Area), which can copy between pages, you can layer content on top of existing output, and still restore the original data afterwards. So this could serve as an alternative solution to #10810. ## Detailed Description of the Pull Request / Additional comments On the original DEC terminals that supported paging, you couldn't have both paging and scrollback at the same time - only the one or the other. But modern terminals typically allow both, so we support that too. The way it works, the currently visible page will be attached to the scrollback, and any content that scrolls off the top will thus be saved. But the background pages will not have scrollback, so their content is lost if it scrolls off the top. And when the screen is resized, only the visible page will be reflowed. Background pages are not affected by a resize until they become active. At that point they just receive the traditional style of resize, where the content is clipped or padded to match the new dimensions. I'm not sure this is the best way to handle resizing, but we can always consider other approaches once people have had a chance to try it out. ## Validation Steps Performed I've added some unit tests covering the new operations, and also done a lot of manual testing. Closes #13892 Tests added/passed --- .github/actions/spelling/expect/expect.txt | 4 + src/cascadia/TerminalCore/Terminal.hpp | 3 +- src/cascadia/TerminalCore/TerminalApi.cpp | 9 +- .../UnitTests_TerminalCore/SelectionTest.cpp | 23 +- .../TerminalApiTest.cpp | 3 +- src/host/outputStream.cpp | 21 +- src/host/outputStream.hpp | 3 +- src/terminal/adapter/DispatchTypes.hpp | 1 + src/terminal/adapter/ITermDispatch.hpp | 6 + src/terminal/adapter/ITerminalApi.hpp | 10 +- src/terminal/adapter/InteractDispatch.cpp | 4 +- src/terminal/adapter/PageManager.cpp | 254 +++++ src/terminal/adapter/PageManager.hpp | 67 ++ src/terminal/adapter/adaptDispatch.cpp | 890 ++++++++++-------- src/terminal/adapter/adaptDispatch.hpp | 32 +- .../adapter/adaptDispatchGraphics.cpp | 18 +- src/terminal/adapter/lib/adapter.vcxproj | 2 + .../adapter/lib/adapter.vcxproj.filters | 6 + src/terminal/adapter/sources.inc | 1 + src/terminal/adapter/termDispatch.hpp | 6 + .../adapter/ut_adapter/adapterTest.cpp | 147 ++- .../parser/OutputStateMachineEngine.cpp | 18 + .../parser/OutputStateMachineEngine.hpp | 6 + 23 files changed, 1059 insertions(+), 475 deletions(-) create mode 100644 src/terminal/adapter/PageManager.cpp create mode 100644 src/terminal/adapter/PageManager.hpp diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index 5c0c14b0df9..1a189277aef 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -406,6 +406,7 @@ DECNKM DECNRCM DECOM decommit +DECPCCM DECPCTERM DECPS DECRARA @@ -414,6 +415,7 @@ DECREQTPARM DECRLM DECRPM DECRQCRA +DECRQDE DECRQM DECRQPSR DECRQSS @@ -2123,6 +2125,7 @@ XIn XManifest XMath xorg +XPan XResource xsi xstyler @@ -2142,6 +2145,7 @@ YCast YCENTER YCount YLimit +YPan YSubstantial YVIRTUALSCREEN YWalk diff --git a/src/cascadia/TerminalCore/Terminal.hpp b/src/cascadia/TerminalCore/Terminal.hpp index 75b822b1386..bf1120f421a 100644 --- a/src/cascadia/TerminalCore/Terminal.hpp +++ b/src/cascadia/TerminalCore/Terminal.hpp @@ -131,8 +131,7 @@ class Microsoft::Terminal::Core::Terminal final : // These methods are defined in TerminalApi.cpp void ReturnResponse(const std::wstring_view response) override; Microsoft::Console::VirtualTerminal::StateMachine& GetStateMachine() noexcept override; - TextBuffer& GetTextBuffer() noexcept override; - til::rect GetViewport() const noexcept override; + BufferState GetBufferAndViewport() noexcept override; void SetViewportPosition(const til::point position) noexcept override; void SetTextAttributes(const TextAttribute& attrs) noexcept override; void SetSystemMode(const Mode mode, const bool enabled) noexcept override; diff --git a/src/cascadia/TerminalCore/TerminalApi.cpp b/src/cascadia/TerminalCore/TerminalApi.cpp index 9b85b18d5ff..aa06c101295 100644 --- a/src/cascadia/TerminalCore/TerminalApi.cpp +++ b/src/cascadia/TerminalCore/TerminalApi.cpp @@ -34,14 +34,9 @@ Microsoft::Console::VirtualTerminal::StateMachine& Terminal::GetStateMachine() n return *_stateMachine; } -TextBuffer& Terminal::GetTextBuffer() noexcept +ITerminalApi::BufferState Terminal::GetBufferAndViewport() noexcept { - return _activeBuffer(); -} - -til::rect Terminal::GetViewport() const noexcept -{ - return til::rect{ _GetMutableViewport().ToInclusive() }; + return { _activeBuffer(), til::rect{ _GetMutableViewport().ToInclusive() }, !_inAltBuffer() }; } void Terminal::SetViewportPosition(const til::point position) noexcept diff --git a/src/cascadia/UnitTests_TerminalCore/SelectionTest.cpp b/src/cascadia/UnitTests_TerminalCore/SelectionTest.cpp index 195eb17fcc3..1e55fb1f6e9 100644 --- a/src/cascadia/UnitTests_TerminalCore/SelectionTest.cpp +++ b/src/cascadia/UnitTests_TerminalCore/SelectionTest.cpp @@ -44,6 +44,11 @@ namespace TerminalCoreUnitTests VERIFY_ARE_EQUAL(selection, expected); } + TextBuffer& GetTextBuffer(Terminal& term) + { + return term.GetBufferAndViewport().buffer; + } + TEST_METHOD(SelectUnit) { Terminal term{ Terminal::TestDummyMarker{} }; @@ -394,7 +399,7 @@ namespace TerminalCoreUnitTests const auto burrito = L"\xD83C\xDF2F"; // Insert wide glyph at position (4,10) - term.GetTextBuffer().GetCursor().SetPosition({ 4, 10 }); + GetTextBuffer(term).GetCursor().SetPosition({ 4, 10 }); term.Write(burrito); // Simulate click at (x,y) = (5,10) @@ -417,7 +422,7 @@ namespace TerminalCoreUnitTests const auto burrito = L"\xD83C\xDF2F"; // Insert wide glyph at position (4,10) - term.GetTextBuffer().GetCursor().SetPosition({ 4, 10 }); + GetTextBuffer(term).GetCursor().SetPosition({ 4, 10 }); term.Write(burrito); // Simulate click at (x,y) = (5,10) @@ -440,11 +445,11 @@ namespace TerminalCoreUnitTests const auto burrito = L"\xD83C\xDF2F"; // Insert wide glyph at position (4,10) - term.GetTextBuffer().GetCursor().SetPosition({ 4, 10 }); + GetTextBuffer(term).GetCursor().SetPosition({ 4, 10 }); term.Write(burrito); // Insert wide glyph at position (7,11) - term.GetTextBuffer().GetCursor().SetPosition({ 7, 11 }); + GetTextBuffer(term).GetCursor().SetPosition({ 7, 11 }); term.Write(burrito); // Simulate ALT + click at (x,y) = (5,8) @@ -496,7 +501,7 @@ namespace TerminalCoreUnitTests // Insert text at position (4,10) const std::wstring_view text = L"doubleClickMe"; - term.GetTextBuffer().GetCursor().SetPosition({ 4, 10 }); + GetTextBuffer(term).GetCursor().SetPosition({ 4, 10 }); term.Write(text); // Simulate double click at (x,y) = (5,10) @@ -540,7 +545,7 @@ namespace TerminalCoreUnitTests // Insert text at position (4,10) const std::wstring_view text = L"C:\\Terminal>"; - term.GetTextBuffer().GetCursor().SetPosition({ 4, 10 }); + GetTextBuffer(term).GetCursor().SetPosition({ 4, 10 }); term.Write(text); // Simulate click at (x,y) = (15,10) @@ -568,7 +573,7 @@ namespace TerminalCoreUnitTests // Insert text at position (4,10) const std::wstring_view text = L"doubleClickMe dragThroughHere"; - term.GetTextBuffer().GetCursor().SetPosition({ 4, 10 }); + GetTextBuffer(term).GetCursor().SetPosition({ 4, 10 }); term.Write(text); // Simulate double click at (x,y) = (5,10) @@ -597,7 +602,7 @@ namespace TerminalCoreUnitTests // Insert text at position (21,10) const std::wstring_view text = L"doubleClickMe dragThroughHere"; - term.GetTextBuffer().GetCursor().SetPosition({ 4, 10 }); + GetTextBuffer(term).GetCursor().SetPosition({ 4, 10 }); term.Write(text); // Simulate double click at (x,y) = (21,10) @@ -685,7 +690,7 @@ namespace TerminalCoreUnitTests // Insert text at position (4,10) const std::wstring_view text = L"doubleClickMe dragThroughHere"; - term.GetTextBuffer().GetCursor().SetPosition({ 4, 10 }); + GetTextBuffer(term).GetCursor().SetPosition({ 4, 10 }); term.Write(text); // Step 1: Create a selection on "doubleClickMe" diff --git a/src/cascadia/UnitTests_TerminalCore/TerminalApiTest.cpp b/src/cascadia/UnitTests_TerminalCore/TerminalApiTest.cpp index 9f002db5098..157bad1805d 100644 --- a/src/cascadia/UnitTests_TerminalCore/TerminalApiTest.cpp +++ b/src/cascadia/UnitTests_TerminalCore/TerminalApiTest.cpp @@ -152,7 +152,8 @@ void TerminalApiTest::CursorVisibility() VERIFY_IS_TRUE(term._mainBuffer->GetCursor().IsOn()); VERIFY_IS_TRUE(term._mainBuffer->GetCursor().IsBlinkingAllowed()); - term.GetTextBuffer().GetCursor().SetIsVisible(false); + auto& textBuffer = term.GetBufferAndViewport().buffer; + textBuffer.GetCursor().SetIsVisible(false); VERIFY_IS_FALSE(term._mainBuffer->GetCursor().IsVisible()); VERIFY_IS_TRUE(term._mainBuffer->GetCursor().IsOn()); VERIFY_IS_TRUE(term._mainBuffer->GetCursor().IsBlinkingAllowed()); diff --git a/src/host/outputStream.cpp b/src/host/outputStream.cpp index fb20d74b75b..151a472f69d 100644 --- a/src/host/outputStream.cpp +++ b/src/host/outputStream.cpp @@ -52,25 +52,16 @@ StateMachine& ConhostInternalGetSet::GetStateMachine() } // Routine Description: -// - Retrieves the text buffer for the active output buffer. +// - Retrieves the text buffer and virtual viewport for the active output +// buffer. Also returns a flag indicating whether it's the main buffer. // Arguments: // - // Return Value: -// - a reference to the TextBuffer instance. -TextBuffer& ConhostInternalGetSet::GetTextBuffer() +// - a tuple with the buffer reference, viewport, and main buffer flag. +ITerminalApi::BufferState ConhostInternalGetSet::GetBufferAndViewport() { - return _io.GetActiveOutputBuffer().GetTextBuffer(); -} - -// Routine Description: -// - Retrieves the virtual viewport of the active output buffer. -// Arguments: -// - -// Return Value: -// - the exclusive coordinates of the viewport. -til::rect ConhostInternalGetSet::GetViewport() const -{ - return _io.GetActiveOutputBuffer().GetVirtualViewport().ToExclusive(); + auto& info = _io.GetActiveOutputBuffer(); + return { info.GetTextBuffer(), info.GetVirtualViewport().ToExclusive(), info.Next == nullptr }; } // Routine Description: diff --git a/src/host/outputStream.hpp b/src/host/outputStream.hpp index 01d8abaf17f..36283087ac2 100644 --- a/src/host/outputStream.hpp +++ b/src/host/outputStream.hpp @@ -32,8 +32,7 @@ class ConhostInternalGetSet final : public Microsoft::Console::VirtualTerminal:: void ReturnResponse(const std::wstring_view response) override; Microsoft::Console::VirtualTerminal::StateMachine& GetStateMachine() override; - TextBuffer& GetTextBuffer() override; - til::rect GetViewport() const override; + BufferState GetBufferAndViewport() override; void SetViewportPosition(const til::point position) override; void SetTextAttributes(const TextAttribute& attrs) override; diff --git a/src/terminal/adapter/DispatchTypes.hpp b/src/terminal/adapter/DispatchTypes.hpp index 52852bed6a2..a0a74d8c83a 100644 --- a/src/terminal/adapter/DispatchTypes.hpp +++ b/src/terminal/adapter/DispatchTypes.hpp @@ -531,6 +531,7 @@ namespace Microsoft::Console::VirtualTerminal::DispatchTypes ATT610_StartCursorBlink = DECPrivateMode(12), DECTCEM_TextCursorEnableMode = DECPrivateMode(25), XTERM_EnableDECCOLMSupport = DECPrivateMode(40), + DECPCCM_PageCursorCouplingMode = DECPrivateMode(64), DECNKM_NumericKeypadMode = DECPrivateMode(66), DECBKM_BackarrowKeyMode = DECPrivateMode(67), DECLRMM_LeftRightMarginMode = DECPrivateMode(69), diff --git a/src/terminal/adapter/ITermDispatch.hpp b/src/terminal/adapter/ITermDispatch.hpp index eed892f5a94..e3976996433 100644 --- a/src/terminal/adapter/ITermDispatch.hpp +++ b/src/terminal/adapter/ITermDispatch.hpp @@ -49,6 +49,12 @@ class Microsoft::Console::VirtualTerminal::ITermDispatch virtual bool DeleteCharacter(const VTInt count) = 0; // DCH virtual bool ScrollUp(const VTInt distance) = 0; // SU virtual bool ScrollDown(const VTInt distance) = 0; // SD + virtual bool NextPage(const VTInt pageCount) = 0; // NP + virtual bool PrecedingPage(const VTInt pageCount) = 0; // PP + virtual bool PagePositionAbsolute(const VTInt page) = 0; // PPA + virtual bool PagePositionRelative(const VTInt pageCount) = 0; // PPR + virtual bool PagePositionBack(const VTInt pageCount) = 0; // PPB + virtual bool RequestDisplayedExtent() = 0; // DECRQDE virtual bool InsertLine(const VTInt distance) = 0; // IL virtual bool DeleteLine(const VTInt distance) = 0; // DL virtual bool InsertColumn(const VTInt distance) = 0; // DECIC diff --git a/src/terminal/adapter/ITerminalApi.hpp b/src/terminal/adapter/ITerminalApi.hpp index 4381c4ecbdd..2c3aec044ed 100644 --- a/src/terminal/adapter/ITerminalApi.hpp +++ b/src/terminal/adapter/ITerminalApi.hpp @@ -39,9 +39,15 @@ namespace Microsoft::Console::VirtualTerminal virtual void ReturnResponse(const std::wstring_view response) = 0; + struct BufferState + { + TextBuffer& buffer; + til::rect viewport; + bool isMainBuffer; + }; + virtual StateMachine& GetStateMachine() = 0; - virtual TextBuffer& GetTextBuffer() = 0; - virtual til::rect GetViewport() const = 0; + virtual BufferState GetBufferAndViewport() = 0; virtual void SetViewportPosition(const til::point position) = 0; virtual bool IsVtInputEnabled() const = 0; diff --git a/src/terminal/adapter/InteractDispatch.cpp b/src/terminal/adapter/InteractDispatch.cpp index cb06c985c91..a102368ec5b 100644 --- a/src/terminal/adapter/InteractDispatch.cpp +++ b/src/terminal/adapter/InteractDispatch.cpp @@ -108,7 +108,7 @@ bool InteractDispatch::WindowManipulation(const DispatchTypes::WindowManipulatio _api.ShowWindow(false); return true; case DispatchTypes::WindowManipulationType::RefreshWindow: - _api.GetTextBuffer().TriggerRedrawAll(); + _api.GetBufferAndViewport().buffer.TriggerRedrawAll(); return true; case DispatchTypes::WindowManipulationType::ResizeWindowInCharacters: // TODO:GH#1765 We should introduce a better `ResizeConpty` function to @@ -135,7 +135,7 @@ bool InteractDispatch::WindowManipulation(const DispatchTypes::WindowManipulatio bool InteractDispatch::MoveCursor(const VTInt row, const VTInt col) { // First retrieve some information about the buffer - const auto viewport = _api.GetViewport(); + const auto viewport = _api.GetBufferAndViewport().viewport; // In VT, the origin is 1,1. For our array, it's 0,0. So subtract 1. // Apply boundary tests to ensure the cursor isn't outside the viewport rectangle. diff --git a/src/terminal/adapter/PageManager.cpp b/src/terminal/adapter/PageManager.cpp new file mode 100644 index 00000000000..5408b249738 --- /dev/null +++ b/src/terminal/adapter/PageManager.cpp @@ -0,0 +1,254 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "PageManager.hpp" +#include "../../renderer/base/renderer.hpp" + +using namespace Microsoft::Console::VirtualTerminal; + +Page::Page(TextBuffer& buffer, const til::rect& viewport, const til::CoordType number) noexcept : + _buffer{ buffer }, + _viewport{ viewport }, + _number(number) +{ +} + +TextBuffer& Page::Buffer() const noexcept +{ + return _buffer; +} + +til::rect Page::Viewport() const noexcept +{ + return _viewport; +} + +til::CoordType Page::Number() const noexcept +{ + return _number; +} + +Cursor& Page::Cursor() const noexcept +{ + return _buffer.GetCursor(); +} + +const TextAttribute& Page::Attributes() const noexcept +{ + return _buffer.GetCurrentAttributes(); +} + +void Page::SetAttributes(const TextAttribute& attr, ITerminalApi* api) const +{ + _buffer.SetCurrentAttributes(attr); + // If the api parameter was specified, we need to pass the new attributes + // through to the api. This occurs when there's a potential for the colors + // to be changed, which may require some legacy remapping in conhost. + if (api) + { + api->SetTextAttributes(attr); + } +} + +til::CoordType Page::Top() const noexcept +{ + // If we ever support vertical window panning, the page top won't + // necessarily align with the viewport top, so it's best we always + // treat them as distinct properties. + return _viewport.top; +} + +til::CoordType Page::Bottom() const noexcept +{ + // Similarly, the page bottom won't always match the viewport bottom. + return _viewport.bottom; +} + +til::CoordType Page::Width() const noexcept +{ + // The page width could also one day be different from the buffer width, + // so again it's best treated as a distinct property. + return _buffer.GetSize().Width(); +} + +til::CoordType Page::Height() const noexcept +{ + return Bottom() - Top(); +} + +til::CoordType Page::BufferHeight() const noexcept +{ + return _buffer.GetSize().Height(); +} + +til::CoordType Page::XPanOffset() const noexcept +{ + return _viewport.left; +} + +til::CoordType Page::YPanOffset() const noexcept +{ + return 0; // Vertical panning is not yet supported +} + +PageManager::PageManager(ITerminalApi& api, Renderer& renderer) noexcept : + _api{ api }, + _renderer{ renderer } +{ +} + +void PageManager::Reset() +{ + _activePageNumber = 1; + _visiblePageNumber = 1; + _buffers = {}; +} + +Page PageManager::Get(const til::CoordType pageNumber) const +{ + const auto requestedPageNumber = std::min(std::max(pageNumber, 1), MAX_PAGES); + auto [visibleBuffer, visibleViewport, isMainBuffer] = _api.GetBufferAndViewport(); + + // If we're not in the main buffer (either because an app has enabled the + // alternate buffer mode, or switched the conhost screen buffer), then VT + // paging doesn't apply, so we disregard the requested page number and just + // use the visible buffer (with a fixed page number of 1). + if (!isMainBuffer) + { + return { visibleBuffer, visibleViewport, 1 }; + } + + // If the requested page number happens to be the visible page, then we + // can also just use the visible buffer as is. + if (requestedPageNumber == _visiblePageNumber) + { + return { visibleBuffer, visibleViewport, _visiblePageNumber }; + } + + // Otherwise we're working with a background buffer, so we need to + // retrieve that from the buffer array, and resize it to match the + // active page size. + const auto pageSize = visibleViewport.size(); + auto& pageBuffer = _getBuffer(requestedPageNumber, pageSize); + return { pageBuffer, til::rect{ pageSize }, requestedPageNumber }; +} + +Page PageManager::ActivePage() const +{ + return Get(_activePageNumber); +} + +Page PageManager::VisiblePage() const +{ + return Get(_visiblePageNumber); +} + +void PageManager::MoveTo(const til::CoordType pageNumber, const bool makeVisible) +{ + auto [visibleBuffer, visibleViewport, isMainBuffer] = _api.GetBufferAndViewport(); + if (!isMainBuffer) + { + return; + } + + const auto pageSize = visibleViewport.size(); + const auto visibleTop = visibleViewport.top; + const auto wasVisible = _activePageNumber == _visiblePageNumber; + const auto newPageNumber = std::min(std::max(pageNumber, 1), MAX_PAGES); + auto redrawRequired = false; + + // If we're changing the visible page, what we do is swap out the current + // visible page into its backing buffer, and swap in the new page from the + // backing buffer to the main buffer. That way the rest of the system only + // ever has to deal with the main buffer. + if (makeVisible && _visiblePageNumber != newPageNumber) + { + const auto& newBuffer = _getBuffer(newPageNumber, pageSize); + auto& saveBuffer = _getBuffer(_visiblePageNumber, pageSize); + for (auto i = 0; i < pageSize.height; i++) + { + saveBuffer.GetMutableRowByOffset(i).CopyFrom(visibleBuffer.GetRowByOffset(visibleTop + i)); + } + for (auto i = 0; i < pageSize.height; i++) + { + visibleBuffer.GetMutableRowByOffset(visibleTop + i).CopyFrom(newBuffer.GetRowByOffset(i)); + } + _visiblePageNumber = newPageNumber; + redrawRequired = true; + } + + // If the active page was previously visible, and is now still visible, + // there is no need to update any buffer properties, because we'll have + // been using the main buffer in both cases. + const auto isVisible = newPageNumber == _visiblePageNumber; + if (!wasVisible || !isVisible) + { + // Otherwise we need to copy the properties from the old buffer to the + // new, so we retain the current attributes and cursor position. This + // is only needed if they are actually different. + auto& oldBuffer = wasVisible ? visibleBuffer : _getBuffer(_activePageNumber, pageSize); + auto& newBuffer = isVisible ? visibleBuffer : _getBuffer(newPageNumber, pageSize); + if (&oldBuffer != &newBuffer) + { + // When copying the cursor position, we need to adjust the y + // coordinate to account for scrollback in the visible buffer. + const auto oldTop = wasVisible ? visibleTop : 0; + const auto newTop = isVisible ? visibleTop : 0; + auto position = oldBuffer.GetCursor().GetPosition(); + position.y = position.y - oldTop + newTop; + newBuffer.SetCurrentAttributes(oldBuffer.GetCurrentAttributes()); + newBuffer.CopyProperties(oldBuffer); + newBuffer.GetCursor().SetPosition(position); + } + // If we moved from the visible buffer to a background buffer we need + // to hide the cursor in the visible buffer. This is because the page + // number is like a third dimension in the cursor coordinate system. + // If the cursor isn't on the visible page, it's the same as if its + // x/y coordinates are outside the visible viewport. + if (wasVisible && !isVisible) + { + visibleBuffer.GetCursor().SetIsVisible(false); + } + } + + _activePageNumber = newPageNumber; + if (redrawRequired) + { + _renderer.TriggerRedrawAll(); + } +} + +void PageManager::MoveRelative(const til::CoordType pageCount, const bool makeVisible) +{ + MoveTo(_activePageNumber + pageCount, makeVisible); +} + +void PageManager::MakeActivePageVisible() +{ + if (_activePageNumber != _visiblePageNumber) + { + MoveTo(_activePageNumber, true); + } +} + +TextBuffer& PageManager::_getBuffer(const til::CoordType pageNumber, const til::size pageSize) const +{ + auto& buffer = til::at(_buffers, pageNumber - 1); + if (buffer == nullptr) + { + // Page buffers are created on demand, and are sized to match the active + // page dimensions without any scrollback rows. + buffer = std::make_unique(pageSize, TextAttribute{}, 0, false, _renderer); + } + else if (buffer->GetSize().Dimensions() != pageSize) + { + // If a buffer already exists for the page, and the page dimensions have + // changed while it was inactive, it will need to be resized. + // TODO: We don't currently reflow the existing content in this case, but + // that may be something we want to reconsider. + buffer->ResizeTraditional(pageSize); + } + return *buffer; +} diff --git a/src/terminal/adapter/PageManager.hpp b/src/terminal/adapter/PageManager.hpp new file mode 100644 index 00000000000..ab913902609 --- /dev/null +++ b/src/terminal/adapter/PageManager.hpp @@ -0,0 +1,67 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- PageManager.hpp + +Abstract: +- This manages the text buffers required by the VT paging operations. +--*/ + +#pragma once + +#include "ITerminalApi.hpp" +#include "til.h" + +namespace Microsoft::Console::VirtualTerminal +{ + class Page + { + public: + Page(TextBuffer& buffer, const til::rect& viewport, const til::CoordType number) noexcept; + TextBuffer& Buffer() const noexcept; + til::rect Viewport() const noexcept; + til::CoordType Number() const noexcept; + Cursor& Cursor() const noexcept; + const TextAttribute& Attributes() const noexcept; + void SetAttributes(const TextAttribute& attr, ITerminalApi* api = nullptr) const; + til::CoordType Top() const noexcept; + til::CoordType Bottom() const noexcept; + til::CoordType Width() const noexcept; + til::CoordType Height() const noexcept; + til::CoordType BufferHeight() const noexcept; + til::CoordType XPanOffset() const noexcept; + til::CoordType YPanOffset() const noexcept; + + private: + TextBuffer& _buffer; + til::rect _viewport; + til::CoordType _number; + }; + + class PageManager + { + using Renderer = Microsoft::Console::Render::Renderer; + + public: + PageManager(ITerminalApi& api, Renderer& renderer) noexcept; + void Reset(); + Page Get(const til::CoordType pageNumber) const; + Page ActivePage() const; + Page VisiblePage() const; + void MoveTo(const til::CoordType pageNumber, const bool makeVisible); + void MoveRelative(const til::CoordType pageCount, const bool makeVisible); + void MakeActivePageVisible(); + + private: + TextBuffer& _getBuffer(const til::CoordType pageNumber, const til::size pageSize) const; + + ITerminalApi& _api; + Renderer& _renderer; + til::CoordType _activePageNumber = 1; + til::CoordType _visiblePageNumber = 1; + static constexpr til::CoordType MAX_PAGES = 6; + mutable std::array, MAX_PAGES> _buffers; + }; +} diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp index 02c22db8eec..4135cac88d6 100644 --- a/src/terminal/adapter/adaptDispatch.cpp +++ b/src/terminal/adapter/adaptDispatch.cpp @@ -22,7 +22,8 @@ AdaptDispatch::AdaptDispatch(ITerminalApi& api, Renderer& renderer, RenderSettin _renderSettings{ renderSettings }, _terminalInput{ terminalInput }, _usingAltBuffer(false), - _termOutput() + _termOutput(), + _pages{ api, renderer } { } @@ -72,15 +73,15 @@ void AdaptDispatch::PrintString(const std::wstring_view string) void AdaptDispatch::_WriteToBuffer(const std::wstring_view string) { - auto& textBuffer = _api.GetTextBuffer(); - auto& cursor = textBuffer.GetCursor(); + const auto page = _pages.ActivePage(); + auto& textBuffer = page.Buffer(); + auto& cursor = page.Cursor(); auto cursorPosition = cursor.GetPosition(); const auto wrapAtEOL = _api.GetSystemMode(ITerminalApi::Mode::AutoWrap); - const auto& attributes = textBuffer.GetCurrentAttributes(); + const auto& attributes = page.Attributes(); - const auto viewport = _api.GetViewport(); - const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true); - const auto [leftMargin, rightMargin] = _GetHorizontalMargins(textBuffer.GetSize().Width()); + const auto [topMargin, bottomMargin] = _GetVerticalMargins(page, true); + const auto [leftMargin, rightMargin] = _GetHorizontalMargins(page.Width()); auto lineWidth = textBuffer.GetLineWidth(cursorPosition.y); if (cursorPosition.x <= rightMargin && cursorPosition.y >= topMargin && cursorPosition.y <= bottomMargin) @@ -106,7 +107,7 @@ void AdaptDispatch::_WriteToBuffer(const std::wstring_view string) // different position from where the EOL was marked. if (delayedCursorPosition == cursorPosition) { - _DoLineFeed(textBuffer, true, true); + _DoLineFeed(page, true, true); cursorPosition = cursor.GetPosition(); // We need to recalculate the width when moving to a new line. lineWidth = textBuffer.GetLineWidth(cursorPosition.y); @@ -266,19 +267,19 @@ bool AdaptDispatch::CursorPrevLine(const VTInt distance) // Routine Description: // - Returns the coordinates of the vertical scroll margins. // Arguments: -// - viewport - The viewport rect (exclusive). -// - absolute - Should coordinates be absolute or relative to the viewport. +// - page - The page that the margins will apply to. +// - absolute - Should coordinates be absolute or relative to the page top. // Return Value: // - A std::pair containing the top and bottom coordinates (inclusive). -std::pair AdaptDispatch::_GetVerticalMargins(const til::rect& viewport, const bool absolute) noexcept +std::pair AdaptDispatch::_GetVerticalMargins(const Page& page, const bool absolute) noexcept { // If the top is out of range, reset the margins completely. - const auto bottommostRow = viewport.bottom - viewport.top - 1; + const auto bottommostRow = page.Height() - 1; if (_scrollMargins.top >= bottommostRow) { _scrollMargins.top = _scrollMargins.bottom = 0; } - // If margins aren't set, use the full extent of the viewport. + // If margins aren't set, use the full extent of the page. const auto marginsSet = _scrollMargins.top < _scrollMargins.bottom; auto topMargin = marginsSet ? _scrollMargins.top : 0; auto bottomMargin = marginsSet ? _scrollMargins.bottom : bottommostRow; @@ -286,8 +287,8 @@ std::pair AdaptDispatch::_GetVerticalMargins(const til::rect& viewport bottomMargin = std::min(bottomMargin, bottommostRow); if (absolute) { - topMargin += viewport.top; - bottomMargin += viewport.top; + topMargin += page.Top(); + bottomMargin += page.Top(); } return { topMargin, bottomMargin }; } @@ -295,13 +296,13 @@ std::pair AdaptDispatch::_GetVerticalMargins(const til::rect& viewport // Routine Description: // - Returns the coordinates of the horizontal scroll margins. // Arguments: -// - bufferWidth - The width of the buffer +// - pageWidth - The width of the page // Return Value: // - A std::pair containing the left and right coordinates (inclusive). -std::pair AdaptDispatch::_GetHorizontalMargins(const til::CoordType bufferWidth) noexcept +std::pair AdaptDispatch::_GetHorizontalMargins(const til::CoordType pageWidth) noexcept { // If the left is out of range, reset the margins completely. - const auto rightmostColumn = bufferWidth - 1; + const auto rightmostColumn = pageWidth - 1; if (_scrollMargins.left >= rightmostColumn) { _scrollMargins.left = _scrollMargins.right = 0; @@ -326,13 +327,12 @@ std::pair AdaptDispatch::_GetHorizontalMargins(const til::CoordType bu bool AdaptDispatch::_CursorMovePosition(const Offset rowOffset, const Offset colOffset, const bool clampInMargins) { // First retrieve some information about the buffer - const auto viewport = _api.GetViewport(); - auto& textBuffer = _api.GetTextBuffer(); - auto& cursor = textBuffer.GetCursor(); - const auto bufferWidth = textBuffer.GetSize().Width(); + const auto page = _pages.ActivePage(); + auto& cursor = page.Cursor(); + const auto pageWidth = page.Width(); const auto cursorPosition = cursor.GetPosition(); - const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true); - const auto [leftMargin, rightMargin] = _GetHorizontalMargins(bufferWidth); + const auto [topMargin, bottomMargin] = _GetVerticalMargins(page, true); + const auto [leftMargin, rightMargin] = _GetHorizontalMargins(pageWidth); // For relative movement, the given offsets will be relative to // the current cursor position. @@ -340,10 +340,10 @@ bool AdaptDispatch::_CursorMovePosition(const Offset rowOffset, const Offset col auto col = cursorPosition.x; // But if the row is absolute, it will be relative to the top of the - // viewport, or the top margin, depending on the origin mode. + // page, or the top margin, depending on the origin mode. if (rowOffset.IsAbsolute) { - row = _modes.test(Mode::Origin) ? topMargin : viewport.top; + row = _modes.test(Mode::Origin) ? topMargin : page.Top(); } // And if the column is absolute, it'll be relative to column 0, @@ -355,10 +355,10 @@ bool AdaptDispatch::_CursorMovePosition(const Offset rowOffset, const Offset col } // Adjust the base position by the given offsets and clamp the results. - // The row is constrained within the viewport's vertical boundaries, + // The row is constrained within the page's vertical boundaries, // while the column is constrained by the buffer width. - row = std::clamp(row + rowOffset.Value, viewport.top, viewport.bottom - 1); - col = std::clamp(col + colOffset.Value, 0, bufferWidth - 1); + row = std::clamp(row + rowOffset.Value, page.Top(), page.Bottom() - 1); + col = std::clamp(col + colOffset.Value, 0, pageWidth - 1); // If the operation needs to be clamped inside the margins, or the origin // mode is relative (which always requires margin clamping), then the row @@ -398,7 +398,7 @@ bool AdaptDispatch::_CursorMovePosition(const Offset rowOffset, const Offset col } // Finally, attempt to set the adjusted cursor position back into the console. - cursor.SetPosition(textBuffer.ClampPositionWithinLine({ col, row })); + cursor.SetPosition(page.Buffer().ClampPositionWithinLine({ col, row })); _ApplyCursorMovementFlags(cursor); return true; @@ -490,29 +490,28 @@ bool AdaptDispatch::CursorPosition(const VTInt line, const VTInt column) bool AdaptDispatch::CursorSaveState() { // First retrieve some information about the buffer - const auto viewport = _api.GetViewport(); - const auto& textBuffer = _api.GetTextBuffer(); - const auto& attributes = textBuffer.GetCurrentAttributes(); + const auto page = _pages.ActivePage(); // The cursor is given to us by the API as relative to the whole buffer. - // But in VT speak, the cursor row should be relative to the current viewport top. - auto cursorPosition = textBuffer.GetCursor().GetPosition(); - cursorPosition.y -= viewport.top; + // But in VT speak, the cursor row should be relative to the current page top. + auto cursorPosition = page.Cursor().GetPosition(); + cursorPosition.y -= page.Top(); // Although if origin mode is set, the cursor is relative to the margin origin. if (_modes.test(Mode::Origin)) { - cursorPosition.x -= _GetHorizontalMargins(textBuffer.GetSize().Width()).first; - cursorPosition.y -= _GetVerticalMargins(viewport, false).first; + cursorPosition.x -= _GetHorizontalMargins(page.Width()).first; + cursorPosition.y -= _GetVerticalMargins(page, false).first; } // VT is also 1 based, not 0 based, so correct by 1. auto& savedCursorState = _savedCursorState.at(_usingAltBuffer); savedCursorState.Column = cursorPosition.x + 1; savedCursorState.Row = cursorPosition.y + 1; - savedCursorState.IsDelayedEOLWrap = textBuffer.GetCursor().IsDelayedEOLWrap(); + savedCursorState.Page = page.Number(); + savedCursorState.IsDelayedEOLWrap = page.Cursor().IsDelayedEOLWrap(); savedCursorState.IsOriginModeRelative = _modes.test(Mode::Origin); - savedCursorState.Attributes = attributes; + savedCursorState.Attributes = page.Attributes(); savedCursorState.TermOutput = _termOutput; return true; @@ -533,17 +532,21 @@ bool AdaptDispatch::CursorRestoreState() // Restore the origin mode first, since the cursor coordinates may be relative. _modes.set(Mode::Origin, savedCursorState.IsOriginModeRelative); + // Restore the page number. + PagePositionAbsolute(savedCursorState.Page); + // We can then restore the position with a standard CUP operation. CursorPosition(savedCursorState.Row, savedCursorState.Column); // If the delayed wrap flag was set when the cursor was saved, we need to restore that now. + const auto page = _pages.ActivePage(); if (savedCursorState.IsDelayedEOLWrap) { - _api.GetTextBuffer().GetCursor().DelayEOLWrap(); + page.Cursor().DelayEOLWrap(); } // Restore text attributes. - _api.SetTextAttributes(savedCursorState.Attributes); + page.SetAttributes(savedCursorState.Attributes, &_api); // Restore designated character sets. _termOutput.RestoreFrom(savedCursorState.TermOutput); @@ -556,10 +559,10 @@ bool AdaptDispatch::CursorRestoreState() // the Erase Color mode is set, we use the default attributes, but when reset, // we use the active color attributes with the character attributes cleared. // Arguments: -// - textBuffer - Target buffer that is being erased. +// - page - Target page that is being erased. // Return Value: // - The erase TextAttribute value. -TextAttribute AdaptDispatch::_GetEraseAttributes(const TextBuffer& textBuffer) const noexcept +TextAttribute AdaptDispatch::_GetEraseAttributes(const Page& page) const noexcept { if (_modes.test(Mode::EraseColor)) { @@ -567,7 +570,7 @@ TextAttribute AdaptDispatch::_GetEraseAttributes(const TextBuffer& textBuffer) c } else { - auto eraseAttributes = textBuffer.GetCurrentAttributes(); + auto eraseAttributes = page.Attributes(); eraseAttributes.SetStandardErase(); return eraseAttributes; } @@ -576,13 +579,14 @@ TextAttribute AdaptDispatch::_GetEraseAttributes(const TextBuffer& textBuffer) c // Routine Description: // - Scrolls an area of the buffer in a vertical direction. // Arguments: -// - textBuffer - Target buffer to be scrolled. -// - fillRect - Area of the buffer that will be affected. +// - page - Target page to be scrolled. +// - fillRect - Area of the page that will be affected. // - delta - Distance to move (positive is down, negative is up). // Return Value: // - -void AdaptDispatch::_ScrollRectVertically(TextBuffer& textBuffer, const til::rect& scrollRect, const VTInt delta) +void AdaptDispatch::_ScrollRectVertically(const Page& page, const til::rect& scrollRect, const VTInt delta) { + auto& textBuffer = page.Buffer(); const auto absoluteDelta = std::min(std::abs(delta), scrollRect.height()); if (absoluteDelta < scrollRect.height()) { @@ -590,7 +594,7 @@ void AdaptDispatch::_ScrollRectVertically(TextBuffer& textBuffer, const til::rec const auto width = scrollRect.width(); const auto height = scrollRect.height() - absoluteDelta; const auto actualDelta = delta > 0 ? absoluteDelta : -absoluteDelta; - if (width == textBuffer.GetSize().Width()) + if (width == page.Width()) { // If the scrollRect is the full width of the buffer, we can scroll // more efficiently by rotating the row storage. @@ -621,8 +625,8 @@ void AdaptDispatch::_ScrollRectVertically(TextBuffer& textBuffer, const til::rec auto eraseRect = scrollRect; eraseRect.top = delta > 0 ? scrollRect.top : (scrollRect.bottom - absoluteDelta); eraseRect.bottom = eraseRect.top + absoluteDelta; - const auto eraseAttributes = _GetEraseAttributes(textBuffer); - _FillRect(textBuffer, eraseRect, whitespace, eraseAttributes); + const auto eraseAttributes = _GetEraseAttributes(page); + _FillRect(page, eraseRect, whitespace, eraseAttributes); // Also reset the line rendition for the erased rows. textBuffer.ResetLineRenditionRange(eraseRect.top, eraseRect.bottom); @@ -631,13 +635,14 @@ void AdaptDispatch::_ScrollRectVertically(TextBuffer& textBuffer, const til::rec // Routine Description: // - Scrolls an area of the buffer in a horizontal direction. // Arguments: -// - textBuffer - Target buffer to be scrolled. -// - fillRect - Area of the buffer that will be affected. +// - page - Target page to be scrolled. +// - fillRect - Area of the page that will be affected. // - delta - Distance to move (positive is right, negative is left). // Return Value: // - -void AdaptDispatch::_ScrollRectHorizontally(TextBuffer& textBuffer, const til::rect& scrollRect, const VTInt delta) +void AdaptDispatch::_ScrollRectHorizontally(const Page& page, const til::rect& scrollRect, const VTInt delta) { + auto& textBuffer = page.Buffer(); const auto absoluteDelta = std::min(std::abs(delta), scrollRect.width()); if (absoluteDelta < scrollRect.width()) { @@ -669,8 +674,8 @@ void AdaptDispatch::_ScrollRectHorizontally(TextBuffer& textBuffer, const til::r auto eraseRect = scrollRect; eraseRect.left = delta > 0 ? scrollRect.left : (scrollRect.right - absoluteDelta); eraseRect.right = eraseRect.left + absoluteDelta; - const auto eraseAttributes = _GetEraseAttributes(textBuffer); - _FillRect(textBuffer, eraseRect, whitespace, eraseAttributes); + const auto eraseAttributes = _GetEraseAttributes(page); + _FillRect(page, eraseRect, whitespace, eraseAttributes); } // Routine Description: @@ -682,20 +687,19 @@ void AdaptDispatch::_ScrollRectHorizontally(TextBuffer& textBuffer, const til::r // - void AdaptDispatch::_InsertDeleteCharacterHelper(const VTInt delta) { - const auto viewport = _api.GetViewport(); - auto& textBuffer = _api.GetTextBuffer(); - const auto row = textBuffer.GetCursor().GetPosition().y; - const auto col = textBuffer.GetCursor().GetPosition().x; - const auto lineWidth = textBuffer.GetLineWidth(row); - const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true); + const auto page = _pages.ActivePage(); + const auto row = page.Cursor().GetPosition().y; + const auto col = page.Cursor().GetPosition().x; + const auto lineWidth = page.Buffer().GetLineWidth(row); + const auto [topMargin, bottomMargin] = _GetVerticalMargins(page, true); const auto [leftMargin, rightMargin] = (row >= topMargin && row <= bottomMargin) ? _GetHorizontalMargins(lineWidth) : std::make_pair(0, lineWidth - 1); if (col >= leftMargin && col <= rightMargin) { - _ScrollRectHorizontally(textBuffer, { col, row, rightMargin + 1, row + 1 }, delta); + _ScrollRectHorizontally(page, { col, row, rightMargin + 1, row + 1 }, delta); // The ICH and DCH controls are expected to reset the delayed wrap flag. - textBuffer.GetCursor().ResetDelayEOLWrap(); + page.Cursor().ResetDelayEOLWrap(); } } @@ -728,15 +732,15 @@ bool AdaptDispatch::DeleteCharacter(const VTInt count) // Routine Description: // - Fills an area of the buffer with a given character and attributes. // Arguments: -// - textBuffer - Target buffer to be filled. -// - fillRect - Area of the buffer that will be affected. +// - page - Target page to be filled. +// - fillRect - Area of the page that will be affected. // - fillChar - Character to be written to the buffer. // - fillAttrs - Attributes to be written to the buffer. // Return Value: // - -void AdaptDispatch::_FillRect(TextBuffer& textBuffer, const til::rect& fillRect, const std::wstring_view& fillChar, const TextAttribute& fillAttrs) const +void AdaptDispatch::_FillRect(const Page& page, const til::rect& fillRect, const std::wstring_view& fillChar, const TextAttribute& fillAttrs) const { - textBuffer.FillRect(fillRect, fillChar, fillAttrs); + page.Buffer().FillRect(fillRect, fillChar, fillAttrs); _api.NotifyAccessibilityChange(fillRect); } @@ -751,28 +755,28 @@ void AdaptDispatch::_FillRect(TextBuffer& textBuffer, const til::rect& fillRect, // - True. bool AdaptDispatch::EraseCharacters(const VTInt numChars) { - auto& textBuffer = _api.GetTextBuffer(); - const auto row = textBuffer.GetCursor().GetPosition().y; - const auto startCol = textBuffer.GetCursor().GetPosition().x; - const auto endCol = std::min(startCol + numChars, textBuffer.GetLineWidth(row)); + const auto page = _pages.ActivePage(); + const auto row = page.Cursor().GetPosition().y; + const auto startCol = page.Cursor().GetPosition().x; + const auto endCol = std::min(startCol + numChars, page.Buffer().GetLineWidth(row)); // The ECH control is expected to reset the delayed wrap flag. - textBuffer.GetCursor().ResetDelayEOLWrap(); + page.Cursor().ResetDelayEOLWrap(); - const auto eraseAttributes = _GetEraseAttributes(textBuffer); - _FillRect(textBuffer, { startCol, row, endCol, row + 1 }, whitespace, eraseAttributes); + const auto eraseAttributes = _GetEraseAttributes(page); + _FillRect(page, { startCol, row, endCol, row + 1 }, whitespace, eraseAttributes); return true; } // Routine Description: -// - ED - Erases a portion of the current viewable area (viewport) of the console. +// - ED - Erases a portion of the current page of the console. // Arguments: // - eraseType - Determines whether to erase: // From beginning (top-left corner) to the cursor // From cursor to end (bottom-right corner) -// The entire viewport area -// The scrollback (outside the viewport area) +// The entire page +// The scrollback (outside the page area) // Return Value: // - True if handled successfully. False otherwise. bool AdaptDispatch::EraseInDisplay(const DispatchTypes::EraseType eraseType) @@ -783,7 +787,7 @@ bool AdaptDispatch::EraseInDisplay(const DispatchTypes::EraseType eraseType) // Scrollback clears erase everything in the "scrollback" of a *nix terminal // Everything that's scrolled off the screen so far. // Or if it's an Erase All, then we also need to handle that specially - // by moving the current contents of the viewport into the scrollback. + // by moving the current contents of the page into the scrollback. if (eraseType == DispatchTypes::EraseType::Scrollback) { return _EraseScrollback(); @@ -793,18 +797,18 @@ bool AdaptDispatch::EraseInDisplay(const DispatchTypes::EraseType eraseType) return _EraseAll(); } - const auto viewport = _api.GetViewport(); - auto& textBuffer = _api.GetTextBuffer(); - const auto bufferWidth = textBuffer.GetSize().Width(); - const auto row = textBuffer.GetCursor().GetPosition().y; - const auto col = textBuffer.GetCursor().GetPosition().x; + const auto page = _pages.ActivePage(); + auto& textBuffer = page.Buffer(); + const auto pageWidth = page.Width(); + const auto row = page.Cursor().GetPosition().y; + const auto col = page.Cursor().GetPosition().x; // The ED control is expected to reset the delayed wrap flag. // The special case variants above ("erase all" and "erase scrollback") // take care of that themselves when they set the cursor position. - textBuffer.GetCursor().ResetDelayEOLWrap(); + page.Cursor().ResetDelayEOLWrap(); - const auto eraseAttributes = _GetEraseAttributes(textBuffer); + const auto eraseAttributes = _GetEraseAttributes(page); // When erasing the display, every line that is erased in full should be // reset to single width. When erasing to the end, this could include @@ -814,15 +818,15 @@ bool AdaptDispatch::EraseInDisplay(const DispatchTypes::EraseType eraseType) // the line is double width). if (eraseType == DispatchTypes::EraseType::FromBeginning) { - textBuffer.ResetLineRenditionRange(viewport.top, row); - _FillRect(textBuffer, { 0, viewport.top, bufferWidth, row }, whitespace, eraseAttributes); - _FillRect(textBuffer, { 0, row, col + 1, row + 1 }, whitespace, eraseAttributes); + textBuffer.ResetLineRenditionRange(page.Top(), row); + _FillRect(page, { 0, page.Top(), pageWidth, row }, whitespace, eraseAttributes); + _FillRect(page, { 0, row, col + 1, row + 1 }, whitespace, eraseAttributes); } if (eraseType == DispatchTypes::EraseType::ToEnd) { - textBuffer.ResetLineRenditionRange(col > 0 ? row + 1 : row, viewport.bottom); - _FillRect(textBuffer, { col, row, bufferWidth, row + 1 }, whitespace, eraseAttributes); - _FillRect(textBuffer, { 0, row + 1, bufferWidth, viewport.bottom }, whitespace, eraseAttributes); + textBuffer.ResetLineRenditionRange(col > 0 ? row + 1 : row, page.Bottom()); + _FillRect(page, { col, row, pageWidth, row + 1 }, whitespace, eraseAttributes); + _FillRect(page, { 0, row + 1, pageWidth, page.Bottom() }, whitespace, eraseAttributes); } return true; @@ -836,24 +840,25 @@ bool AdaptDispatch::EraseInDisplay(const DispatchTypes::EraseType eraseType) // - True if handled successfully. False otherwise. bool AdaptDispatch::EraseInLine(const DispatchTypes::EraseType eraseType) { - auto& textBuffer = _api.GetTextBuffer(); - const auto row = textBuffer.GetCursor().GetPosition().y; - const auto col = textBuffer.GetCursor().GetPosition().x; + const auto page = _pages.ActivePage(); + const auto& textBuffer = page.Buffer(); + const auto row = page.Cursor().GetPosition().y; + const auto col = page.Cursor().GetPosition().x; // The EL control is expected to reset the delayed wrap flag. - textBuffer.GetCursor().ResetDelayEOLWrap(); + page.Cursor().ResetDelayEOLWrap(); - const auto eraseAttributes = _GetEraseAttributes(textBuffer); + const auto eraseAttributes = _GetEraseAttributes(page); switch (eraseType) { case DispatchTypes::EraseType::FromBeginning: - _FillRect(textBuffer, { 0, row, col + 1, row + 1 }, whitespace, eraseAttributes); + _FillRect(page, { 0, row, col + 1, row + 1 }, whitespace, eraseAttributes); return true; case DispatchTypes::EraseType::ToEnd: - _FillRect(textBuffer, { col, row, textBuffer.GetLineWidth(row), row + 1 }, whitespace, eraseAttributes); + _FillRect(page, { col, row, textBuffer.GetLineWidth(row), row + 1 }, whitespace, eraseAttributes); return true; case DispatchTypes::EraseType::All: - _FillRect(textBuffer, { 0, row, textBuffer.GetLineWidth(row), row + 1 }, whitespace, eraseAttributes); + _FillRect(page, { 0, row, textBuffer.GetLineWidth(row), row + 1 }, whitespace, eraseAttributes); return true; default: return false; @@ -863,17 +868,17 @@ bool AdaptDispatch::EraseInLine(const DispatchTypes::EraseType eraseType) // Routine Description: // - Selectively erases unprotected cells in an area of the buffer. // Arguments: -// - textBuffer - Target buffer to be erased. -// - eraseRect - Area of the buffer that will be affected. +// - page - Target page to be erased. +// - eraseRect - Area of the page that will be affected. // Return Value: // - -void AdaptDispatch::_SelectiveEraseRect(TextBuffer& textBuffer, const til::rect& eraseRect) +void AdaptDispatch::_SelectiveEraseRect(const Page& page, const til::rect& eraseRect) { if (eraseRect) { for (auto row = eraseRect.top; row < eraseRect.bottom; row++) { - auto& rowBuffer = textBuffer.GetMutableRowByOffset(row); + auto& rowBuffer = page.Buffer().GetMutableRowByOffset(row); for (auto col = eraseRect.left; col < eraseRect.right; col++) { // Only unprotected cells are affected. @@ -881,7 +886,7 @@ void AdaptDispatch::_SelectiveEraseRect(TextBuffer& textBuffer, const til::rect& { // The text is cleared but the attributes are left as is. rowBuffer.ClearCell(col); - textBuffer.TriggerRedraw(Viewport::FromCoord({ col, row })); + page.Buffer().TriggerRedraw(Viewport::FromCoord({ col, row })); } } } @@ -890,37 +895,36 @@ void AdaptDispatch::_SelectiveEraseRect(TextBuffer& textBuffer, const til::rect& } // Routine Description: -// - DECSED - Selectively erases unprotected cells in a portion of the viewport. +// - DECSED - Selectively erases unprotected cells in a portion of the page. // Arguments: // - eraseType - Determines whether to erase: // From beginning (top-left corner) to the cursor // From cursor to end (bottom-right corner) -// The entire viewport area +// The entire page area // Return Value: // - True if handled successfully. False otherwise. bool AdaptDispatch::SelectiveEraseInDisplay(const DispatchTypes::EraseType eraseType) { - const auto viewport = _api.GetViewport(); - auto& textBuffer = _api.GetTextBuffer(); - const auto bufferWidth = textBuffer.GetSize().Width(); - const auto row = textBuffer.GetCursor().GetPosition().y; - const auto col = textBuffer.GetCursor().GetPosition().x; + const auto page = _pages.ActivePage(); + const auto pageWidth = page.Width(); + const auto row = page.Cursor().GetPosition().y; + const auto col = page.Cursor().GetPosition().x; // The DECSED control is expected to reset the delayed wrap flag. - textBuffer.GetCursor().ResetDelayEOLWrap(); + page.Cursor().ResetDelayEOLWrap(); switch (eraseType) { case DispatchTypes::EraseType::FromBeginning: - _SelectiveEraseRect(textBuffer, { 0, viewport.top, bufferWidth, row }); - _SelectiveEraseRect(textBuffer, { 0, row, col + 1, row + 1 }); + _SelectiveEraseRect(page, { 0, page.Top(), pageWidth, row }); + _SelectiveEraseRect(page, { 0, row, col + 1, row + 1 }); return true; case DispatchTypes::EraseType::ToEnd: - _SelectiveEraseRect(textBuffer, { col, row, bufferWidth, row + 1 }); - _SelectiveEraseRect(textBuffer, { 0, row + 1, bufferWidth, viewport.bottom }); + _SelectiveEraseRect(page, { col, row, pageWidth, row + 1 }); + _SelectiveEraseRect(page, { 0, row + 1, pageWidth, page.Bottom() }); return true; case DispatchTypes::EraseType::All: - _SelectiveEraseRect(textBuffer, { 0, viewport.top, bufferWidth, viewport.bottom }); + _SelectiveEraseRect(page, { 0, page.Top(), pageWidth, page.Bottom() }); return true; default: return false; @@ -938,23 +942,24 @@ bool AdaptDispatch::SelectiveEraseInDisplay(const DispatchTypes::EraseType erase // - True if handled successfully. False otherwise. bool AdaptDispatch::SelectiveEraseInLine(const DispatchTypes::EraseType eraseType) { - auto& textBuffer = _api.GetTextBuffer(); - const auto row = textBuffer.GetCursor().GetPosition().y; - const auto col = textBuffer.GetCursor().GetPosition().x; + const auto page = _pages.ActivePage(); + const auto& textBuffer = page.Buffer(); + const auto row = page.Cursor().GetPosition().y; + const auto col = page.Cursor().GetPosition().x; // The DECSEL control is expected to reset the delayed wrap flag. - textBuffer.GetCursor().ResetDelayEOLWrap(); + page.Cursor().ResetDelayEOLWrap(); switch (eraseType) { case DispatchTypes::EraseType::FromBeginning: - _SelectiveEraseRect(textBuffer, { 0, row, col + 1, row + 1 }); + _SelectiveEraseRect(page, { 0, row, col + 1, row + 1 }); return true; case DispatchTypes::EraseType::ToEnd: - _SelectiveEraseRect(textBuffer, { col, row, textBuffer.GetLineWidth(row), row + 1 }); + _SelectiveEraseRect(page, { col, row, textBuffer.GetLineWidth(row), row + 1 }); return true; case DispatchTypes::EraseType::All: - _SelectiveEraseRect(textBuffer, { 0, row, textBuffer.GetLineWidth(row), row + 1 }); + _SelectiveEraseRect(page, { 0, row, textBuffer.GetLineWidth(row), row + 1 }); return true; default: return false; @@ -964,18 +969,18 @@ bool AdaptDispatch::SelectiveEraseInLine(const DispatchTypes::EraseType eraseTyp // Routine Description: // - Changes the attributes of each cell in a rectangular area of the buffer. // Arguments: -// - textBuffer - Target buffer to be changed. -// - changeRect - A rectangular area of the buffer that will be affected. +// - page - Target page to be changed. +// - changeRect - A rectangular area of the page that will be affected. // - changeOps - Changes that will be applied to each of the attributes. // Return Value: // - -void AdaptDispatch::_ChangeRectAttributes(TextBuffer& textBuffer, const til::rect& changeRect, const ChangeOps& changeOps) +void AdaptDispatch::_ChangeRectAttributes(const Page& page, const til::rect& changeRect, const ChangeOps& changeOps) { if (changeRect) { for (auto row = changeRect.top; row < changeRect.bottom; row++) { - auto& rowBuffer = textBuffer.GetMutableRowByOffset(row); + auto& rowBuffer = page.Buffer().GetMutableRowByOffset(row); for (auto col = changeRect.left; col < changeRect.right; col++) { auto attr = rowBuffer.GetAttrByColumn(col); @@ -998,7 +1003,7 @@ void AdaptDispatch::_ChangeRectAttributes(TextBuffer& textBuffer, const til::rec rowBuffer.ReplaceAttributes(col, col + 1, attr); } } - textBuffer.TriggerRedraw(Viewport::FromExclusive(changeRect)); + page.Buffer().TriggerRedraw(Viewport::FromExclusive(changeRect)); _api.NotifyAccessibilityChange(changeRect); } } @@ -1014,16 +1019,15 @@ void AdaptDispatch::_ChangeRectAttributes(TextBuffer& textBuffer, const til::rec // - void AdaptDispatch::_ChangeRectOrStreamAttributes(const til::rect& changeArea, const ChangeOps& changeOps) { - auto& textBuffer = _api.GetTextBuffer(); - const auto bufferSize = textBuffer.GetSize().Dimensions(); - const auto changeRect = _CalculateRectArea(changeArea.top, changeArea.left, changeArea.bottom, changeArea.right, bufferSize); + const auto page = _pages.ActivePage(); + const auto changeRect = _CalculateRectArea(page, changeArea.top, changeArea.left, changeArea.bottom, changeArea.right); const auto lineCount = changeRect.height(); // If the change extent is rectangular, we can apply the change with a // single call. The same is true for a stream extent that is only one line. if (_modes.test(Mode::RectangularChangeExtent) || lineCount == 1) { - _ChangeRectAttributes(textBuffer, changeRect, changeOps); + _ChangeRectAttributes(page, changeRect, changeOps); } // If the stream extent is more than one line we require three passes. The // top line is altered from the left offset up to the end of the line. The @@ -1032,10 +1036,10 @@ void AdaptDispatch::_ChangeRectOrStreamAttributes(const til::rect& changeArea, c // must be greater than the left, otherwise the operation is ignored. else if (lineCount > 1 && changeRect.right > changeRect.left) { - const auto bufferWidth = bufferSize.width; - _ChangeRectAttributes(textBuffer, { changeRect.origin(), til::size{ bufferWidth - changeRect.left, 1 } }, changeOps); - _ChangeRectAttributes(textBuffer, { { 0, changeRect.top + 1 }, til::size{ bufferWidth, lineCount - 2 } }, changeOps); - _ChangeRectAttributes(textBuffer, { { 0, changeRect.bottom - 1 }, til::size{ changeRect.right, 1 } }, changeOps); + const auto pageWidth = page.Width(); + _ChangeRectAttributes(page, { changeRect.origin(), til::size{ pageWidth - changeRect.left, 1 } }, changeOps); + _ChangeRectAttributes(page, { { 0, changeRect.top + 1 }, til::size{ pageWidth, lineCount - 2 } }, changeOps); + _ChangeRectAttributes(page, { { 0, changeRect.bottom - 1 }, til::size{ changeRect.right, 1 } }, changeOps); } } @@ -1043,25 +1047,26 @@ void AdaptDispatch::_ChangeRectOrStreamAttributes(const til::rect& changeArea, c // - Helper method to calculate the applicable buffer coordinates for use with // the various rectangular area operations. // Arguments: +// - page - The target page. // - top - The first row of the area. // - left - The first column of the area. // - bottom - The last row of the area (inclusive). // - right - The last column of the area (inclusive). -// - bufferSize - The size of the target buffer. // Return value: // - An exclusive rect with the absolute buffer coordinates. -til::rect AdaptDispatch::_CalculateRectArea(const VTInt top, const VTInt left, const VTInt bottom, const VTInt right, const til::size bufferSize) +til::rect AdaptDispatch::_CalculateRectArea(const Page& page, const VTInt top, const VTInt left, const VTInt bottom, const VTInt right) { - const auto viewport = _api.GetViewport(); + const auto pageWidth = page.Width(); + const auto pageHeight = page.Height(); // We start by calculating the margin offsets and maximum dimensions. - // If the origin mode isn't set, we use the viewport extent. - const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, false); - const auto [leftMargin, rightMargin] = _GetHorizontalMargins(bufferSize.width); + // If the origin mode isn't set, we use the page extent. + const auto [topMargin, bottomMargin] = _GetVerticalMargins(page, false); + const auto [leftMargin, rightMargin] = _GetHorizontalMargins(pageWidth); const auto yOffset = _modes.test(Mode::Origin) ? topMargin : 0; - const auto yMaximum = _modes.test(Mode::Origin) ? bottomMargin + 1 : viewport.height(); + const auto yMaximum = _modes.test(Mode::Origin) ? bottomMargin + 1 : pageHeight; const auto xOffset = _modes.test(Mode::Origin) ? leftMargin : 0; - const auto xMaximum = _modes.test(Mode::Origin) ? rightMargin + 1 : bufferSize.width; + const auto xMaximum = _modes.test(Mode::Origin) ? rightMargin + 1 : pageWidth; auto fillRect = til::inclusive_rect{}; fillRect.left = left + xOffset; @@ -1077,9 +1082,9 @@ til::rect AdaptDispatch::_CalculateRectArea(const VTInt top, const VTInt left, c fillRect.top = std::min(fillRect.top, yMaximum) - 1; fillRect.bottom = std::min(fillRect.bottom, yMaximum) - 1; - // To get absolute coordinates we offset with the viewport top. - fillRect.top += viewport.top; - fillRect.bottom += viewport.top; + // To get absolute coordinates we offset with the page top. + fillRect.top += page.Top(); + fillRect.bottom += page.Top(); return til::rect{ fillRect }; } @@ -1198,25 +1203,22 @@ bool AdaptDispatch::ReverseAttributesRectangularArea(const VTInt top, const VTIn // - left - The first column of the source area. // - bottom - The last row of the source area (inclusive). // - right - The last column of the source area (inclusive). -// - page - The source page number (unused for now). +// - page - The source page number. // - dstTop - The first row of the destination. // - dstLeft - The first column of the destination. -// - dstPage - The destination page number (unused for now). +// - dstPage - The destination page number. // Return Value: // - True. -bool AdaptDispatch::CopyRectangularArea(const VTInt top, const VTInt left, const VTInt bottom, const VTInt right, const VTInt /*page*/, const VTInt dstTop, const VTInt dstLeft, const VTInt /*dstPage*/) +bool AdaptDispatch::CopyRectangularArea(const VTInt top, const VTInt left, const VTInt bottom, const VTInt right, const VTInt page, const VTInt dstTop, const VTInt dstLeft, const VTInt dstPage) { - // GH#13892 We don't yet support the paging extension, so for now we ignore - // the page parameters. This is the same as if the maximum page count was 1. - - auto& textBuffer = _api.GetTextBuffer(); - const auto bufferSize = textBuffer.GetSize().Dimensions(); - const auto srcRect = _CalculateRectArea(top, left, bottom, right, bufferSize); + const auto src = _pages.Get(page); + const auto dst = _pages.Get(dstPage); + const auto srcRect = _CalculateRectArea(src, top, left, bottom, right); const auto dstBottom = dstTop + srcRect.height() - 1; const auto dstRight = dstLeft + srcRect.width() - 1; - const auto dstRect = _CalculateRectArea(dstTop, dstLeft, dstBottom, dstRight, bufferSize); + const auto dstRect = _CalculateRectArea(dst, dstTop, dstLeft, dstBottom, dstRight); - if (dstRect && dstRect.origin() != srcRect.origin()) + if (dstRect && (dstRect.origin() != srcRect.origin() || src.Number() != dst.Number())) { // If the source is bigger than the available space at the destination // it needs to be clipped, so we only care about the destination size. @@ -1228,18 +1230,18 @@ bool AdaptDispatch::CopyRectangularArea(const VTInt top, const VTInt left, const // Note that we read two cells from the source before we start writing // to the target, so a two-cell DBCS character can't accidentally delete // itself when moving one cell horizontally. - auto next = OutputCell(*textBuffer.GetCellDataAt(srcPos)); + auto next = OutputCell(*src.Buffer().GetCellDataAt(srcPos)); do { const auto current = next; const auto currentSrcPos = srcPos; srcView.WalkInBounds(srcPos, walkDirection); - next = OutputCell(*textBuffer.GetCellDataAt(srcPos)); + next = OutputCell(*src.Buffer().GetCellDataAt(srcPos)); // If the source position is offscreen (which can occur on double // width lines), then we shouldn't copy anything to the destination. - if (currentSrcPos.x < textBuffer.GetLineWidth(currentSrcPos.y)) + if (currentSrcPos.x < src.Buffer().GetLineWidth(currentSrcPos.y)) { - textBuffer.WriteLine(OutputCellIterator({ ¤t, 1 }), dstPos); + dst.Buffer().WriteLine(OutputCellIterator({ ¤t, 1 }), dstPos); } } while (dstView.WalkInBounds(dstPos, walkDirection)); _api.NotifyAccessibilityChange(dstRect); @@ -1261,8 +1263,8 @@ bool AdaptDispatch::CopyRectangularArea(const VTInt top, const VTInt left, const // - True. bool AdaptDispatch::FillRectangularArea(const VTParameter ch, const VTInt top, const VTInt left, const VTInt bottom, const VTInt right) { - auto& textBuffer = _api.GetTextBuffer(); - const auto fillRect = _CalculateRectArea(top, left, bottom, right, textBuffer.GetSize().Dimensions()); + const auto page = _pages.ActivePage(); + const auto fillRect = _CalculateRectArea(page, top, left, bottom, right); // The standard only allows for characters in the range of the GL and GR // character set tables, but we also support additional Unicode characters @@ -1274,8 +1276,8 @@ bool AdaptDispatch::FillRectangularArea(const VTParameter ch, const VTInt top, c if (glChar || grChar || unicodeChar) { const auto fillChar = _termOutput.TranslateKey(gsl::narrow_cast(charValue)); - const auto& fillAttributes = textBuffer.GetCurrentAttributes(); - _FillRect(textBuffer, fillRect, { &fillChar, 1 }, fillAttributes); + const auto& fillAttributes = page.Attributes(); + _FillRect(page, fillRect, { &fillChar, 1 }, fillAttributes); } return true; @@ -1293,10 +1295,10 @@ bool AdaptDispatch::FillRectangularArea(const VTParameter ch, const VTInt top, c // - True. bool AdaptDispatch::EraseRectangularArea(const VTInt top, const VTInt left, const VTInt bottom, const VTInt right) { - auto& textBuffer = _api.GetTextBuffer(); - const auto eraseRect = _CalculateRectArea(top, left, bottom, right, textBuffer.GetSize().Dimensions()); - const auto eraseAttributes = _GetEraseAttributes(textBuffer); - _FillRect(textBuffer, eraseRect, whitespace, eraseAttributes); + const auto page = _pages.ActivePage(); + const auto eraseRect = _CalculateRectArea(page, top, left, bottom, right); + const auto eraseAttributes = _GetEraseAttributes(page); + _FillRect(page, eraseRect, whitespace, eraseAttributes); return true; } @@ -1312,9 +1314,9 @@ bool AdaptDispatch::EraseRectangularArea(const VTInt top, const VTInt left, cons // - True. bool AdaptDispatch::SelectiveEraseRectangularArea(const VTInt top, const VTInt left, const VTInt bottom, const VTInt right) { - auto& textBuffer = _api.GetTextBuffer(); - const auto eraseRect = _CalculateRectArea(top, left, bottom, right, textBuffer.GetSize().Dimensions()); - _SelectiveEraseRect(textBuffer, eraseRect); + const auto page = _pages.ActivePage(); + const auto eraseRect = _CalculateRectArea(page, top, left, bottom, right); + _SelectiveEraseRect(page, eraseRect); return true; } @@ -1346,7 +1348,7 @@ bool AdaptDispatch::SelectAttributeChangeExtent(const DispatchTypes::ChangeExten // the buffer memory. // Arguments: // - id - a numeric label used to identify the request. -// - page - The page number (unused for now). +// - page - The page number. // - top - The first row of the area. // - left - The first column of the area. // - bottom - The last row of the area (inclusive). @@ -1359,7 +1361,9 @@ bool AdaptDispatch::RequestChecksumRectangularArea(const VTInt id, const VTInt p // If this feature is not enabled, we'll just report a zero checksum. if constexpr (Feature_VtChecksumReport::IsEnabled()) { - if (page == 1) + // If the page number is 0, then we're meant to return a checksum of all + // of the pages, but we have no need for that, so we'll just return 0. + if (page != 0) { // As part of the checksum, we need to include the color indices of each // cell, and in the case of default colors, those indices come from the @@ -1370,8 +1374,8 @@ bool AdaptDispatch::RequestChecksumRectangularArea(const VTInt id, const VTInt p defaultFgIndex = defaultFgIndex < 16 ? defaultFgIndex : 7; defaultBgIndex = defaultBgIndex < 16 ? defaultBgIndex : 0; - const auto& textBuffer = _api.GetTextBuffer(); - const auto eraseRect = _CalculateRectArea(top, left, bottom, right, textBuffer.GetSize().Dimensions()); + const auto target = _pages.Get(page); + const auto eraseRect = _CalculateRectArea(target, top, left, bottom, right); for (auto row = eraseRect.top; row < eraseRect.bottom; row++) { for (auto col = eraseRect.left; col < eraseRect.right; col++) @@ -1381,7 +1385,7 @@ bool AdaptDispatch::RequestChecksumRectangularArea(const VTInt id, const VTInt p // predate Unicode, though, so we'd need a custom mapping table // to lookup the correct checksums. Considering this is only for // testing at the moment, that doesn't seem worth the effort. - const auto cell = textBuffer.GetCellDataAt({ col, row }); + const auto cell = target.Buffer().GetCellDataAt({ col, row }); for (auto ch : cell->Chars()) { // That said, I've made a special allowance for U+2426, @@ -1431,14 +1435,14 @@ bool AdaptDispatch::SetLineRendition(const LineRendition rendition) // The line rendition can't be changed if left/right margins are allowed. if (!_modes.test(Mode::AllowDECSLRM)) { - auto& textBuffer = _api.GetTextBuffer(); - const auto eraseAttributes = _GetEraseAttributes(textBuffer); - textBuffer.SetCurrentLineRendition(rendition, eraseAttributes); + const auto page = _pages.ActivePage(); + const auto eraseAttributes = _GetEraseAttributes(page); + page.Buffer().SetCurrentLineRendition(rendition, eraseAttributes); // There is some variation in how this was handled by the different DEC // terminals, but the STD 070 reference (on page D-13) makes it clear that // the delayed wrap (aka the Last Column Flag) was expected to be reset when // line rendition controls were executed. - textBuffer.GetCursor().ResetDelayEOLWrap(); + page.Cursor().ResetDelayEOLWrap(); } return true; } @@ -1630,7 +1634,7 @@ void AdaptDispatch::_DeviceStatusReport(const wchar_t* parameters) const } // Routine Description: -// - CPR and DECXCPR- Reports the current cursor position within the viewport, +// - CPR and DECXCPR- Reports the current cursor position within the page, // as well as the current page number if this is an extended report. // Arguments: // - extendedReport - Set to true if the report should include the page number @@ -1638,32 +1642,30 @@ void AdaptDispatch::_DeviceStatusReport(const wchar_t* parameters) const // - void AdaptDispatch::_CursorPositionReport(const bool extendedReport) { - const auto viewport = _api.GetViewport(); - const auto& textBuffer = _api.GetTextBuffer(); + const auto page = _pages.ActivePage(); // First pull the cursor position relative to the entire buffer out of the console. - til::point cursorPosition{ textBuffer.GetCursor().GetPosition() }; + til::point cursorPosition{ page.Cursor().GetPosition() }; - // Now adjust it for its position in respect to the current viewport top. - cursorPosition.y -= viewport.top; + // Now adjust it for its position in respect to the current page top. + cursorPosition.y -= page.Top(); - // NOTE: 1,1 is the top-left corner of the viewport in VT-speak, so add 1. + // NOTE: 1,1 is the top-left corner of the page in VT-speak, so add 1. cursorPosition.x++; cursorPosition.y++; // If the origin mode is set, the cursor is relative to the margin origin. if (_modes.test(Mode::Origin)) { - cursorPosition.x -= _GetHorizontalMargins(textBuffer.GetSize().Width()).first; - cursorPosition.y -= _GetVerticalMargins(viewport, false).first; + cursorPosition.x -= _GetHorizontalMargins(page.Width()).first; + cursorPosition.y -= _GetVerticalMargins(page, false).first; } // Now send it back into the input channel of the console. if (extendedReport) { - // An extended report should also include the page number, but for now - // we hard-code it to 1, since we don't yet support paging (GH#13892). - const auto pageNumber = 1; + // An extended report also includes the page number. + const auto pageNumber = page.Number(); const auto response = wil::str_printf(L"\x1b[?%d;%d;%dR", cursorPosition.y, cursorPosition.x, pageNumber); _api.ReturnResponse(response); } @@ -1711,12 +1713,10 @@ void AdaptDispatch::_MacroChecksumReport(const VTParameter id) const // - void AdaptDispatch::_ScrollMovement(const VTInt delta) { - const auto viewport = _api.GetViewport(); - auto& textBuffer = _api.GetTextBuffer(); - const auto bufferWidth = textBuffer.GetSize().Width(); - const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true); - const auto [leftMargin, rightMargin] = _GetHorizontalMargins(bufferWidth); - _ScrollRectVertically(textBuffer, { leftMargin, topMargin, rightMargin + 1, bottomMargin + 1 }, delta); + const auto page = _pages.ActivePage(); + const auto [topMargin, bottomMargin] = _GetVerticalMargins(page, true); + const auto [leftMargin, rightMargin] = _GetHorizontalMargins(page.Width()); + _ScrollRectVertically(page, { leftMargin, topMargin, rightMargin + 1, bottomMargin + 1 }, delta); } // Routine Description: @@ -1743,6 +1743,88 @@ bool AdaptDispatch::ScrollDown(const VTInt uiDistance) return true; } +// Routine Description: +// - NP - Moves the active position one or more pages ahead, and moves the +// cursor to home. +// Arguments: +// - pageCount - Number of pages to move +// Return Value: +// - True. +bool AdaptDispatch::NextPage(const VTInt pageCount) +{ + PagePositionRelative(pageCount); + return CursorPosition(1, 1); +} + +// Routine Description: +// - PP - Moves the active position one or more pages back, and moves the +// cursor to home. +// Arguments: +// - pageCount - Number of pages to move +// Return Value: +// - True. +bool AdaptDispatch::PrecedingPage(const VTInt pageCount) +{ + PagePositionBack(pageCount); + return CursorPosition(1, 1); +} + +// Routine Description: +// - PPA - Moves the active position to the specified page number, without +// altering the cursor coordinates. +// Arguments: +// - page - Destination page +// Return Value: +// - True. +bool AdaptDispatch::PagePositionAbsolute(const VTInt page) +{ + _pages.MoveTo(page, _modes.test(Mode::PageCursorCoupling)); + return true; +} + +// Routine Description: +// - PPR - Moves the active position one or more pages ahead, without altering +// the cursor coordinates. +// Arguments: +// - pageCount - Number of pages to move +// Return Value: +// - True. +bool AdaptDispatch::PagePositionRelative(const VTInt pageCount) +{ + _pages.MoveRelative(pageCount, _modes.test(Mode::PageCursorCoupling)); + return true; +} + +// Routine Description: +// - PPB - Moves the active position one or more pages back, without altering +// the cursor coordinates. +// Arguments: +// - pageCount - Number of pages to move +// Return Value: +// - True. +bool AdaptDispatch::PagePositionBack(const VTInt pageCount) +{ + _pages.MoveRelative(-pageCount, _modes.test(Mode::PageCursorCoupling)); + return true; +} + +// Routine Description: +// - DECRQDE - Requests the area of page memory that is currently visible. +// Arguments: +// - None +// Return Value: +// - True. +bool AdaptDispatch::RequestDisplayedExtent() +{ + const auto page = _pages.VisiblePage(); + const auto width = page.Viewport().width(); + const auto height = page.Viewport().height(); + const auto left = page.XPanOffset() + 1; + const auto top = page.YPanOffset() + 1; + _api.ReturnResponse(fmt::format(FMT_COMPILE(L"\033[{};{};{};{};{}\"w"), height, width, left, top, page.Number())); + return true; +} + // Routine Description: // - DECCOLM not only sets the number of columns, but also clears the screen buffer, // resets the page margins and origin mode, and places the cursor at 1,1 @@ -1755,10 +1837,10 @@ void AdaptDispatch::_SetColumnMode(const bool enable) // Only proceed if DECCOLM is allowed. Return true, as this is technically a successful handling. if (_modes.test(Mode::AllowDECCOLM) && !_api.IsConsolePty()) { - const auto viewport = _api.GetViewport(); - const auto viewportHeight = viewport.bottom - viewport.top; - const auto viewportWidth = (enable ? DispatchTypes::s_sDECCOLMSetColumns : DispatchTypes::s_sDECCOLMResetColumns); - _api.ResizeWindow(viewportWidth, viewportHeight); + const auto page = _pages.VisiblePage(); + const auto pageHeight = page.Height(); + const auto pageWidth = (enable ? DispatchTypes::s_sDECCOLMSetColumns : DispatchTypes::s_sDECCOLMResetColumns); + _api.ResizeWindow(pageWidth, pageHeight); _modes.set(Mode::Column, enable); _modes.reset(Mode::Origin, Mode::AllowDECSLRM); CursorPosition(1, 1); @@ -1781,8 +1863,8 @@ void AdaptDispatch::_SetAlternateScreenBufferMode(const bool enable) if (enable) { CursorSaveState(); - const auto& textBuffer = _api.GetTextBuffer(); - _api.UseAlternateScreenBuffer(_GetEraseAttributes(textBuffer)); + const auto page = _pages.ActivePage(); + _api.UseAlternateScreenBuffer(_GetEraseAttributes(page)); _usingAltBuffer = true; } else @@ -1860,21 +1942,28 @@ bool AdaptDispatch::_ModeParamsHelper(const DispatchTypes::ModeParams param, con // Resetting DECAWM should also reset the delayed wrap flag. if (!enable) { - _api.GetTextBuffer().GetCursor().ResetDelayEOLWrap(); + _pages.ActivePage().Cursor().ResetDelayEOLWrap(); } return true; case DispatchTypes::ModeParams::DECARM_AutoRepeatMode: _terminalInput.SetInputMode(TerminalInput::Mode::AutoRepeat, enable); return !_PassThroughInputModes(); case DispatchTypes::ModeParams::ATT610_StartCursorBlink: - _api.GetTextBuffer().GetCursor().SetBlinkingAllowed(enable); + _pages.ActivePage().Cursor().SetBlinkingAllowed(enable); return !_api.IsConsolePty(); case DispatchTypes::ModeParams::DECTCEM_TextCursorEnableMode: - _api.GetTextBuffer().GetCursor().SetIsVisible(enable); + _pages.ActivePage().Cursor().SetIsVisible(enable); return true; case DispatchTypes::ModeParams::XTERM_EnableDECCOLMSupport: _modes.set(Mode::AllowDECCOLM, enable); return true; + case DispatchTypes::ModeParams::DECPCCM_PageCursorCouplingMode: + _modes.set(Mode::PageCursorCoupling, enable); + if (enable) + { + _pages.MakeActivePageVisible(); + } + return true; case DispatchTypes::ModeParams::DECNKM_NumericKeypadMode: _terminalInput.SetInputMode(TerminalInput::Mode::Keypad, enable); return !_PassThroughInputModes(); @@ -1887,9 +1976,8 @@ bool AdaptDispatch::_ModeParamsHelper(const DispatchTypes::ModeParams param, con if (enable) { // If we've allowed left/right margins, we can't have line renditions. - const auto viewport = _api.GetViewport(); - auto& textBuffer = _api.GetTextBuffer(); - textBuffer.ResetLineRenditionRange(viewport.top, viewport.bottom); + const auto page = _pages.ActivePage(); + page.Buffer().ResetLineRenditionRange(page.Top(), page.Bottom()); } return true; case DispatchTypes::ModeParams::DECECM_EraseColorMode: @@ -2011,10 +2099,10 @@ bool AdaptDispatch::RequestMode(const DispatchTypes::ModeParams param) enabled = _terminalInput.GetInputMode(TerminalInput::Mode::AutoRepeat); break; case DispatchTypes::ModeParams::ATT610_StartCursorBlink: - enabled = _api.GetTextBuffer().GetCursor().IsBlinkingAllowed(); + enabled = _pages.ActivePage().Cursor().IsBlinkingAllowed(); break; case DispatchTypes::ModeParams::DECTCEM_TextCursorEnableMode: - enabled = _api.GetTextBuffer().GetCursor().IsVisible(); + enabled = _pages.ActivePage().Cursor().IsVisible(); break; case DispatchTypes::ModeParams::XTERM_EnableDECCOLMSupport: // DECCOLM is not supported in conpty mode @@ -2023,6 +2111,9 @@ bool AdaptDispatch::RequestMode(const DispatchTypes::ModeParams param) enabled = _modes.test(Mode::AllowDECCOLM); } break; + case DispatchTypes::ModeParams::DECPCCM_PageCursorCouplingMode: + enabled = _modes.test(Mode::PageCursorCoupling); + break; case DispatchTypes::ModeParams::DECNKM_NumericKeypadMode: enabled = _terminalInput.GetInputMode(TerminalInput::Mode::Keypad); break; @@ -2100,20 +2191,17 @@ bool AdaptDispatch::SetKeypadMode(const bool fApplicationMode) // - void AdaptDispatch::_InsertDeleteLineHelper(const VTInt delta) { - const auto viewport = _api.GetViewport(); - auto& textBuffer = _api.GetTextBuffer(); - const auto bufferWidth = textBuffer.GetSize().Width(); - - auto& cursor = textBuffer.GetCursor(); + const auto page = _pages.ActivePage(); + auto& cursor = page.Cursor(); const auto col = cursor.GetPosition().x; const auto row = cursor.GetPosition().y; - const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true); - const auto [leftMargin, rightMargin] = _GetHorizontalMargins(bufferWidth); + const auto [topMargin, bottomMargin] = _GetVerticalMargins(page, true); + const auto [leftMargin, rightMargin] = _GetHorizontalMargins(page.Width()); if (row >= topMargin && row <= bottomMargin && col >= leftMargin && col <= rightMargin) { // We emulate inserting and deleting by scrolling the area between the cursor and the bottom margin. - _ScrollRectVertically(textBuffer, { leftMargin, row, rightMargin + 1, bottomMargin + 1 }, delta); + _ScrollRectVertically(page, { leftMargin, row, rightMargin + 1, bottomMargin + 1 }, delta); // The IL and DL controls are also expected to move the cursor to the left margin. cursor.SetXPosition(leftMargin); @@ -2161,20 +2249,17 @@ bool AdaptDispatch::DeleteLine(const VTInt distance) // - void AdaptDispatch::_InsertDeleteColumnHelper(const VTInt delta) { - const auto viewport = _api.GetViewport(); - auto& textBuffer = _api.GetTextBuffer(); - const auto bufferWidth = textBuffer.GetSize().Width(); - - const auto& cursor = textBuffer.GetCursor(); + const auto page = _pages.ActivePage(); + const auto& cursor = page.Cursor(); const auto col = cursor.GetPosition().x; const auto row = cursor.GetPosition().y; - const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true); - const auto [leftMargin, rightMargin] = _GetHorizontalMargins(bufferWidth); + const auto [topMargin, bottomMargin] = _GetVerticalMargins(page, true); + const auto [leftMargin, rightMargin] = _GetHorizontalMargins(page.Width()); if (row >= topMargin && row <= bottomMargin && col >= leftMargin && col <= rightMargin) { // We emulate inserting and deleting by scrolling the area between the cursor and the right margin. - _ScrollRectHorizontally(textBuffer, { col, topMargin, rightMargin + 1, bottomMargin + 1 }, delta); + _ScrollRectHorizontally(page, { col, topMargin, rightMargin + 1, bottomMargin + 1 }, delta); } } @@ -2246,23 +2331,23 @@ void AdaptDispatch::_DoSetTopBottomScrollingMargins(const VTInt topMargin, til::CoordType actualTop = topMargin; til::CoordType actualBottom = bottomMargin; - const auto viewport = _api.GetViewport(); - const auto screenHeight = viewport.bottom - viewport.top; + const auto page = _pages.ActivePage(); + const auto pageHeight = page.Height(); // The default top margin is line 1 if (actualTop == 0) { actualTop = 1; } - // The default bottom margin is the screen height + // The default bottom margin is the page height if (actualBottom == 0) { - actualBottom = screenHeight; + actualBottom = pageHeight; } // The top margin must be less than the bottom margin, and the - // bottom margin must be less than or equal to the screen height - if (actualTop < actualBottom && actualBottom <= screenHeight) + // bottom margin must be less than or equal to the page height + if (actualTop < actualBottom && actualBottom <= pageHeight) { - if (actualTop == 1 && actualBottom == screenHeight) + if (actualTop == 1 && actualBottom == pageHeight) { // Client requests setting margins to the entire screen // - clear them instead of setting them. @@ -2323,23 +2408,23 @@ void AdaptDispatch::_DoSetLeftRightScrollingMargins(const VTInt leftMargin, til::CoordType actualLeft = leftMargin; til::CoordType actualRight = rightMargin; - const auto& textBuffer = _api.GetTextBuffer(); - const auto bufferWidth = textBuffer.GetSize().Width(); + const auto page = _pages.ActivePage(); + const auto pageWidth = page.Width(); // The default left margin is column 1 if (actualLeft == 0) { actualLeft = 1; } - // The default right margin is the buffer width + // The default right margin is the page width if (actualRight == 0) { - actualRight = bufferWidth; + actualRight = pageWidth; } // The left margin must be less than the right margin, and the // right margin must be less than or equal to the buffer width - if (actualLeft < actualRight && actualRight <= bufferWidth) + if (actualLeft < actualRight && actualRight <= pageWidth) { - if (actualLeft == 1 && actualRight == bufferWidth) + if (actualLeft == 1 && actualRight == pageWidth) { // Client requests setting margins to the entire screen // - clear them instead of setting them. @@ -2416,20 +2501,20 @@ bool AdaptDispatch::CarriageReturn() // Routine Description: // - Helper method for executing a line feed, possibly preceded by carriage return. // Arguments: -// - textBuffer - Target buffer on which the line feed is executed. +// - page - Target page on which the line feed is executed. // - withReturn - Set to true if a carriage return should be performed as well. // - wrapForced - Set to true is the line feed was the result of the line wrapping. // Return Value: // - -void AdaptDispatch::_DoLineFeed(TextBuffer& textBuffer, const bool withReturn, const bool wrapForced) +void AdaptDispatch::_DoLineFeed(const Page& page, const bool withReturn, const bool wrapForced) { - const auto viewport = _api.GetViewport(); - const auto bufferWidth = textBuffer.GetSize().Width(); - const auto bufferHeight = textBuffer.GetSize().Height(); - const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true); - const auto [leftMargin, rightMargin] = _GetHorizontalMargins(bufferWidth); + auto& textBuffer = page.Buffer(); + const auto pageWidth = page.Width(); + const auto bufferHeight = page.BufferHeight(); + const auto [topMargin, bottomMargin] = _GetVerticalMargins(page, true); + const auto [leftMargin, rightMargin] = _GetHorizontalMargins(pageWidth); - auto& cursor = textBuffer.GetCursor(); + auto& cursor = page.Cursor(); const auto currentPosition = cursor.GetPosition(); auto newPosition = currentPosition; @@ -2451,36 +2536,36 @@ void AdaptDispatch::_DoLineFeed(TextBuffer& textBuffer, const bool withReturn, c { // If we're not at the bottom margin, or outside the horizontal margins, // then there's no scrolling, so we make sure we don't move past the - // bottom of the viewport. - newPosition.y = std::min(currentPosition.y + 1, viewport.bottom - 1); + // bottom of the page. + newPosition.y = std::min(currentPosition.y + 1, page.Bottom() - 1); newPosition = textBuffer.ClampPositionWithinLine(newPosition); } - else if (topMargin > viewport.top || leftMargin > 0 || rightMargin < bufferWidth - 1) + else if (topMargin > page.Top() || leftMargin > 0 || rightMargin < pageWidth - 1) { - // If the top margin isn't at the top of the viewport, or the + // If the top margin isn't at the top of the page, or the // horizontal margins are set, then we're just scrolling the margin // area and the cursor stays where it is. - _ScrollRectVertically(textBuffer, { leftMargin, topMargin, rightMargin + 1, bottomMargin + 1 }, -1); + _ScrollRectVertically(page, { leftMargin, topMargin, rightMargin + 1, bottomMargin + 1 }, -1); } - else if (viewport.bottom < bufferHeight) + else if (page.Bottom() < bufferHeight) { - // If the top margin is at the top of the viewport, then we'll scroll + // If the top margin is at the top of the page, then we'll scroll // the content up by panning the viewport down, and also move the cursor // down a row. But we only do this if the viewport hasn't yet reached // the end of the buffer. - _api.SetViewportPosition({ viewport.left, viewport.top + 1 }); + _api.SetViewportPosition({ page.XPanOffset(), page.Top() + 1 }); newPosition.y++; - // And if the bottom margin didn't cover the full viewport, we copy the - // lower part of the viewport down so it remains static. But for a full + // And if the bottom margin didn't cover the full page, we copy the + // lower part of the page down so it remains static. But for a full // pan we reset the newly revealed row with the erase attributes. - if (bottomMargin < viewport.bottom - 1) + if (bottomMargin < page.Bottom() - 1) { - _ScrollRectVertically(textBuffer, { 0, bottomMargin + 1, bufferWidth, viewport.bottom + 1 }, 1); + _ScrollRectVertically(page, { 0, bottomMargin + 1, pageWidth, page.Bottom() + 1 }, 1); } else { - const auto eraseAttributes = _GetEraseAttributes(textBuffer); + const auto eraseAttributes = _GetEraseAttributes(page); textBuffer.GetMutableRowByOffset(newPosition.y).Reset(eraseAttributes); } } @@ -2489,7 +2574,7 @@ void AdaptDispatch::_DoLineFeed(TextBuffer& textBuffer, const bool withReturn, c // If the viewport has reached the end of the buffer, we can't pan down, // so we cycle the row coordinates, which effectively scrolls the buffer // content up. In this case we don't need to move the cursor down. - const auto eraseAttributes = _GetEraseAttributes(textBuffer); + const auto eraseAttributes = _GetEraseAttributes(page); textBuffer.IncrementCircularBuffer(eraseAttributes); _api.NotifyBufferRotation(1); @@ -2499,11 +2584,11 @@ void AdaptDispatch::_DoLineFeed(TextBuffer& textBuffer, const bool withReturn, c cursor.SetIsOn(false); textBuffer.TriggerScroll({ 0, -1 }); - // And again, if the bottom margin didn't cover the full viewport, we - // copy the lower part of the viewport down so it remains static. - if (bottomMargin < viewport.bottom - 1) + // And again, if the bottom margin didn't cover the full page, we + // copy the lower part of the page down so it remains static. + if (bottomMargin < page.Bottom() - 1) { - _ScrollRectVertically(textBuffer, { 0, bottomMargin, bufferWidth, bufferHeight }, 1); + _ScrollRectVertically(page, { 0, bottomMargin, pageWidth, bufferHeight }, 1); } } @@ -2520,17 +2605,17 @@ void AdaptDispatch::_DoLineFeed(TextBuffer& textBuffer, const bool withReturn, c // - True if handled successfully. False otherwise. bool AdaptDispatch::LineFeed(const DispatchTypes::LineFeedType lineFeedType) { - auto& textBuffer = _api.GetTextBuffer(); + const auto page = _pages.ActivePage(); switch (lineFeedType) { case DispatchTypes::LineFeedType::DependsOnMode: - _DoLineFeed(textBuffer, _api.GetSystemMode(ITerminalApi::Mode::LineFeed), false); + _DoLineFeed(page, _api.GetSystemMode(ITerminalApi::Mode::LineFeed), false); return true; case DispatchTypes::LineFeedType::WithoutReturn: - _DoLineFeed(textBuffer, false, false); + _DoLineFeed(page, false, false); return true; case DispatchTypes::LineFeedType::WithReturn: - _DoLineFeed(textBuffer, true, false); + _DoLineFeed(page, true, false); return true; default: return false; @@ -2546,23 +2631,22 @@ bool AdaptDispatch::LineFeed(const DispatchTypes::LineFeedType lineFeedType) // - True. bool AdaptDispatch::ReverseLineFeed() { - const auto viewport = _api.GetViewport(); - auto& textBuffer = _api.GetTextBuffer(); - auto& cursor = textBuffer.GetCursor(); + const auto page = _pages.ActivePage(); + const auto& textBuffer = page.Buffer(); + auto& cursor = page.Cursor(); const auto cursorPosition = cursor.GetPosition(); - const auto bufferWidth = textBuffer.GetSize().Width(); - const auto [leftMargin, rightMargin] = _GetHorizontalMargins(bufferWidth); - const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true); + const auto [leftMargin, rightMargin] = _GetHorizontalMargins(page.Width()); + const auto [topMargin, bottomMargin] = _GetVerticalMargins(page, true); // If the cursor is at the top of the margin area, we shift the buffer // contents down, to emulate inserting a line at that point. if (cursorPosition.y == topMargin && cursorPosition.x >= leftMargin && cursorPosition.x <= rightMargin) { - _ScrollRectVertically(textBuffer, { leftMargin, topMargin, rightMargin + 1, bottomMargin + 1 }, 1); + _ScrollRectVertically(page, { leftMargin, topMargin, rightMargin + 1, bottomMargin + 1 }, 1); } - else if (cursorPosition.y > viewport.top) + else if (cursorPosition.y > page.Top()) { - // Otherwise we move the cursor up, but not past the top of the viewport. + // Otherwise we move the cursor up, but not past the top of the page. cursor.SetPosition(textBuffer.ClampPositionWithinLine({ cursorPosition.x, cursorPosition.y - 1 })); _ApplyCursorMovementFlags(cursor); } @@ -2578,18 +2662,16 @@ bool AdaptDispatch::ReverseLineFeed() // - True. bool AdaptDispatch::BackIndex() { - const auto viewport = _api.GetViewport(); - auto& textBuffer = _api.GetTextBuffer(); - auto& cursor = textBuffer.GetCursor(); + const auto page = _pages.ActivePage(); + auto& cursor = page.Cursor(); const auto cursorPosition = cursor.GetPosition(); - const auto bufferWidth = textBuffer.GetSize().Width(); - const auto [leftMargin, rightMargin] = _GetHorizontalMargins(bufferWidth); - const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true); + const auto [leftMargin, rightMargin] = _GetHorizontalMargins(page.Width()); + const auto [topMargin, bottomMargin] = _GetVerticalMargins(page, true); // If the cursor is at the left of the margin area, we shift the buffer right. if (cursorPosition.x == leftMargin && cursorPosition.y >= topMargin && cursorPosition.y <= bottomMargin) { - _ScrollRectHorizontally(textBuffer, { leftMargin, topMargin, rightMargin + 1, bottomMargin + 1 }, 1); + _ScrollRectHorizontally(page, { leftMargin, topMargin, rightMargin + 1, bottomMargin + 1 }, 1); } // Otherwise we move the cursor left, but not past the start of the line. else if (cursorPosition.x > 0) @@ -2609,21 +2691,19 @@ bool AdaptDispatch::BackIndex() // - True. bool AdaptDispatch::ForwardIndex() { - const auto viewport = _api.GetViewport(); - auto& textBuffer = _api.GetTextBuffer(); - auto& cursor = textBuffer.GetCursor(); + const auto page = _pages.ActivePage(); + auto& cursor = page.Cursor(); const auto cursorPosition = cursor.GetPosition(); - const auto bufferWidth = textBuffer.GetSize().Width(); - const auto [leftMargin, rightMargin] = _GetHorizontalMargins(bufferWidth); - const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true); + const auto [leftMargin, rightMargin] = _GetHorizontalMargins(page.Width()); + const auto [topMargin, bottomMargin] = _GetVerticalMargins(page, true); // If the cursor is at the right of the margin area, we shift the buffer left. if (cursorPosition.x == rightMargin && cursorPosition.y >= topMargin && cursorPosition.y <= bottomMargin) { - _ScrollRectHorizontally(textBuffer, { leftMargin, topMargin, rightMargin + 1, bottomMargin + 1 }, -1); + _ScrollRectHorizontally(page, { leftMargin, topMargin, rightMargin + 1, bottomMargin + 1 }, -1); } // Otherwise we move the cursor right, but not past the end of the line. - else if (cursorPosition.x < textBuffer.GetLineWidth(cursorPosition.y) - 1) + else if (cursorPosition.x < page.Buffer().GetLineWidth(cursorPosition.y) - 1) { cursor.SetXPosition(cursorPosition.x + 1); _ApplyCursorMovementFlags(cursor); @@ -2651,11 +2731,10 @@ bool AdaptDispatch::SetWindowTitle(std::wstring_view title) // - True. bool AdaptDispatch::HorizontalTabSet() { - const auto& textBuffer = _api.GetTextBuffer(); - const auto width = textBuffer.GetSize().Dimensions().width; - const auto column = textBuffer.GetCursor().GetPosition().x; + const auto page = _pages.ActivePage(); + const auto column = page.Cursor().GetPosition().x; - _InitTabStopsForWidth(width); + _InitTabStopsForWidth(page.Width()); _tabStopColumns.at(column) = true; return true; @@ -2672,15 +2751,14 @@ bool AdaptDispatch::HorizontalTabSet() // - True. bool AdaptDispatch::ForwardTab(const VTInt numTabs) { - auto& textBuffer = _api.GetTextBuffer(); - auto& cursor = textBuffer.GetCursor(); + const auto page = _pages.ActivePage(); + auto& cursor = page.Cursor(); auto column = cursor.GetPosition().x; const auto row = cursor.GetPosition().y; - const auto width = textBuffer.GetLineWidth(row); + const auto width = page.Buffer().GetLineWidth(row); auto tabsPerformed = 0; - const auto viewport = _api.GetViewport(); - const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true); + const auto [topMargin, bottomMargin] = _GetVerticalMargins(page, true); const auto [leftMargin, rightMargin] = _GetHorizontalMargins(width); const auto clampToMargin = row >= topMargin && row <= bottomMargin && column <= rightMargin; const auto maxColumn = clampToMargin ? rightMargin : width - 1; @@ -2721,15 +2799,14 @@ bool AdaptDispatch::ForwardTab(const VTInt numTabs) // - True. bool AdaptDispatch::BackwardsTab(const VTInt numTabs) { - auto& textBuffer = _api.GetTextBuffer(); - auto& cursor = textBuffer.GetCursor(); + const auto page = _pages.ActivePage(); + auto& cursor = page.Cursor(); auto column = cursor.GetPosition().x; const auto row = cursor.GetPosition().y; - const auto width = textBuffer.GetLineWidth(row); + const auto width = page.Buffer().GetLineWidth(row); auto tabsPerformed = 0; - const auto viewport = _api.GetViewport(); - const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true); + const auto [topMargin, bottomMargin] = _GetVerticalMargins(page, true); const auto [leftMargin, rightMargin] = _GetHorizontalMargins(width); const auto clampToMargin = row >= topMargin && row <= bottomMargin && column >= leftMargin; const auto minColumn = clampToMargin ? leftMargin : 0; @@ -2780,11 +2857,10 @@ bool AdaptDispatch::TabClear(const DispatchTypes::TabClearType clearType) // - void AdaptDispatch::_ClearSingleTabStop() { - const auto& textBuffer = _api.GetTextBuffer(); - const auto width = textBuffer.GetSize().Dimensions().width; - const auto column = textBuffer.GetCursor().GetPosition().x; + const auto page = _pages.ActivePage(); + const auto column = page.Cursor().GetPosition().x; - _InitTabStopsForWidth(width); + _InitTabStopsForWidth(page.Width()); _tabStopColumns.at(column) = false; } @@ -3021,7 +3097,7 @@ bool AdaptDispatch::AnnounceCodeStructure(const VTInt ansiLevel) // True if handled successfully. False otherwise. bool AdaptDispatch::SoftReset() { - _api.GetTextBuffer().GetCursor().SetIsVisible(true); // Cursor enabled. + _pages.ActivePage().Cursor().SetIsVisible(true); // Cursor enabled. // Replace mode; Absolute cursor addressing; Disallow left/right margins. _modes.reset(Mode::InsertReplace, Mode::Origin, Mode::AllowDECSLRM); @@ -3086,6 +3162,9 @@ bool AdaptDispatch::HardReset() _usingAltBuffer = false; } + // Reset all page buffers. + _pages.Reset(); + // Completely reset the TerminalOutput state. _termOutput = {}; if (_initialCodePage.has_value()) @@ -3126,7 +3205,7 @@ bool AdaptDispatch::HardReset() _api.SetSystemMode(ITerminalApi::Mode::BracketedPaste, false); // Restore cursor blinking mode. - _api.GetTextBuffer().GetCursor().SetBlinkingAllowed(true); + _pages.ActivePage().Cursor().SetBlinkingAllowed(true); // Delete all current tab stops and reapply TabSet(DispatchTypes::TabSetType::SetEvery8Columns); @@ -3136,7 +3215,7 @@ bool AdaptDispatch::HardReset() _fontBuffer = nullptr; // Reset internal modes to their initial state - _modes = {}; + _modes = { Mode::PageCursorCoupling }; // Clear and release the macro buffer. if (_macroBuffer) @@ -3172,18 +3251,16 @@ bool AdaptDispatch::HardReset() // - True. bool AdaptDispatch::ScreenAlignmentPattern() { - const auto viewport = _api.GetViewport(); - auto& textBuffer = _api.GetTextBuffer(); - const auto bufferWidth = textBuffer.GetSize().Dimensions().width; + const auto page = _pages.ActivePage(); // Fill the screen with the letter E using the default attributes. - _FillRect(textBuffer, { 0, viewport.top, bufferWidth, viewport.bottom }, L"E", {}); + _FillRect(page, { 0, page.Top(), page.Width(), page.Bottom() }, L"E", {}); // Reset the line rendition for all of these rows. - textBuffer.ResetLineRenditionRange(viewport.top, viewport.bottom); + page.Buffer().ResetLineRenditionRange(page.Top(), page.Bottom()); // Reset the meta/extended attributes (but leave the colors unchanged). - auto attr = textBuffer.GetCurrentAttributes(); + auto attr = page.Attributes(); attr.SetStandardErase(); - _api.SetTextAttributes(attr); + page.SetAttributes(attr); // Reset the origin mode to absolute, and disallow left/right margins. _modes.reset(Mode::Origin, Mode::AllowDECSLRM); // Clear the scrolling margins. @@ -3198,8 +3275,8 @@ bool AdaptDispatch::ScreenAlignmentPattern() //Routine Description: // - Erase Scrollback (^[[3J - ED extension by xterm) // Because conhost doesn't exactly have a scrollback, We have to be tricky here. -// We need to move the entire viewport to 0,0, and clear everything outside -// (0, 0, viewportWidth, viewportHeight) To give the appearance that +// We need to move the entire page to 0,0, and clear everything outside +// (0, 0, pageWidth, pageHeight) To give the appearance that // everything above the viewport was cleared. // We don't want to save the text BELOW the viewport, because in *nix, there isn't anything there // (There isn't a scroll-forward, only a scrollback) @@ -3209,19 +3286,15 @@ bool AdaptDispatch::ScreenAlignmentPattern() // - True if handled successfully. False otherwise. bool AdaptDispatch::_EraseScrollback() { - const auto viewport = _api.GetViewport(); - const auto top = viewport.top; - const auto height = viewport.bottom - viewport.top; - auto& textBuffer = _api.GetTextBuffer(); - const auto bufferSize = textBuffer.GetSize().Dimensions(); - auto& cursor = textBuffer.GetCursor(); + const auto page = _pages.VisiblePage(); + auto& cursor = page.Cursor(); const auto row = cursor.GetPosition().y; - textBuffer.ClearScrollback(top, height); + page.Buffer().ClearScrollback(page.Top(), page.Height()); // Move the viewport - _api.SetViewportPosition({ viewport.left, 0 }); + _api.SetViewportPosition({ page.XPanOffset(), 0 }); // Move the cursor to the same relative location. - cursor.SetYPosition(row - top); + cursor.SetYPosition(row - page.Top()); cursor.SetHasMoved(true); // GH#2715 - If this succeeded, but we're in a conpty, return `false` to @@ -3245,25 +3318,26 @@ bool AdaptDispatch::_EraseScrollback() // - True if handled successfully. False otherwise. bool AdaptDispatch::_EraseAll() { - const auto viewport = _api.GetViewport(); - const auto viewportHeight = viewport.bottom - viewport.top; - auto& textBuffer = _api.GetTextBuffer(); - const auto bufferSize = textBuffer.GetSize(); + const auto page = _pages.ActivePage(); + const auto pageWidth = page.Width(); + const auto pageHeight = page.Height(); + const auto bufferHeight = page.BufferHeight(); + auto& textBuffer = page.Buffer(); const auto inPtyMode = _api.IsConsolePty(); - // Stash away the current position of the cursor within the viewport. + // Stash away the current position of the cursor within the page. // We'll need to restore the cursor to that same relative position, after // we move the viewport. - auto& cursor = textBuffer.GetCursor(); - const auto row = cursor.GetPosition().y - viewport.top; + auto& cursor = page.Cursor(); + const auto row = cursor.GetPosition().y - page.Top(); - // Calculate new viewport position. Typically we want to move one line below + // Calculate new page position. Typically we want to move one line below // the last non-space row, but if the last non-space character is the very // start of the buffer, then we shouldn't move down at all. const auto lastChar = textBuffer.GetLastNonSpaceCharacter(); - auto newViewportTop = lastChar == til::point{} ? 0 : lastChar.y + 1; - auto newViewportBottom = newViewportTop + viewportHeight; - const auto delta = newViewportBottom - (bufferSize.Height()); + auto newPageTop = lastChar == til::point{} ? 0 : lastChar.y + 1; + auto newPageBottom = newPageTop + pageHeight; + const auto delta = newPageBottom - bufferHeight; if (delta > 0) { for (auto i = 0; i < delta; i++) @@ -3271,8 +3345,8 @@ bool AdaptDispatch::_EraseAll() textBuffer.IncrementCircularBuffer(); } _api.NotifyBufferRotation(delta); - newViewportTop -= delta; - newViewportBottom -= delta; + newPageTop -= delta; + newPageBottom -= delta; // We don't want to trigger a scroll in pty mode, because we're going to // pass through the ED sequence anyway, and this will just result in the // buffer being scrolled up by two pages instead of one. @@ -3281,26 +3355,31 @@ bool AdaptDispatch::_EraseAll() textBuffer.TriggerScroll({ 0, -delta }); } } - // Move the viewport - _api.SetViewportPosition({ viewport.left, newViewportTop }); + // Move the viewport if necessary. + if (newPageTop != page.Top()) + { + _api.SetViewportPosition({ page.XPanOffset(), newPageTop }); + } // Restore the relative cursor position - cursor.SetYPosition(row + newViewportTop); + cursor.SetYPosition(row + newPageTop); cursor.SetHasMoved(true); - // Erase all the rows in the current viewport. - const auto eraseAttributes = _GetEraseAttributes(textBuffer); - _FillRect(textBuffer, { 0, newViewportTop, bufferSize.Width(), newViewportBottom }, whitespace, eraseAttributes); + // Erase all the rows in the current page. + const auto eraseAttributes = _GetEraseAttributes(page); + _FillRect(page, { 0, newPageTop, pageWidth, newPageBottom }, whitespace, eraseAttributes); // Also reset the line rendition for the erased rows. - textBuffer.ResetLineRenditionRange(newViewportTop, newViewportBottom); + textBuffer.ResetLineRenditionRange(newPageTop, newPageBottom); // GH#5683 - If this succeeded, but we're in a conpty, return `false` to // make the state machine propagate this ED sequence to the connected // terminal application. While we're in conpty mode, when the client // requests a Erase All operation, we need to manually tell the // connected terminal to do the same thing, so that the terminal will - // move it's own buffer contents into the scrollback. - return !inPtyMode; + // move it's own buffer contents into the scrollback. But this only + // applies if we're in the active buffer, since this should have no + // visible effect for an inactive buffer. + return !(inPtyMode && textBuffer.IsActiveBuffer()); } //Routine Description: @@ -3353,7 +3432,7 @@ bool AdaptDispatch::SetCursorStyle(const DispatchTypes::CursorStyle cursorStyle) return false; } - auto& cursor = _api.GetTextBuffer().GetCursor(); + auto& cursor = _pages.ActivePage().Cursor(); cursor.SetType(actualType); cursor.SetBlinkingAllowed(fEnableBlinking); @@ -3515,14 +3594,17 @@ bool AdaptDispatch::WindowManipulation(const DispatchTypes::WindowManipulationTy _api.ShowWindow(false); return true; case DispatchTypes::WindowManipulationType::RefreshWindow: - _api.GetTextBuffer().TriggerRedrawAll(); + _pages.VisiblePage().Buffer().TriggerRedrawAll(); return true; case DispatchTypes::WindowManipulationType::ResizeWindowInCharacters: _api.ResizeWindow(parameter2.value_or(0), parameter1.value_or(0)); return true; case DispatchTypes::WindowManipulationType::ReportTextSizeInCharacters: - _api.ReturnResponse(fmt::format(FMT_COMPILE(L"\033[8;{};{}t"), _api.GetViewport().height(), _api.GetTextBuffer().GetSize().Width())); + { + const auto page = _pages.VisiblePage(); + _api.ReturnResponse(fmt::format(FMT_COMPILE(L"\033[8;{};{}t"), page.Height(), page.Width())); return true; + } default: return false; } @@ -3536,12 +3618,12 @@ bool AdaptDispatch::WindowManipulation(const DispatchTypes::WindowManipulationTy // - true bool AdaptDispatch::AddHyperlink(const std::wstring_view uri, const std::wstring_view params) { - auto& textBuffer = _api.GetTextBuffer(); - auto attr = textBuffer.GetCurrentAttributes(); - const auto id = textBuffer.GetHyperlinkId(uri, params); + const auto page = _pages.ActivePage(); + auto attr = page.Attributes(); + const auto id = page.Buffer().GetHyperlinkId(uri, params); attr.SetHyperlinkId(id); - textBuffer.SetCurrentAttributes(attr); - textBuffer.AddHyperlinkToMap(uri, id); + page.SetAttributes(attr); + page.Buffer().AddHyperlinkToMap(uri, id); return true; } @@ -3551,10 +3633,10 @@ bool AdaptDispatch::AddHyperlink(const std::wstring_view uri, const std::wstring // - true bool AdaptDispatch::EndHyperlink() { - auto& textBuffer = _api.GetTextBuffer(); - auto attr = textBuffer.GetCurrentAttributes(); + const auto page = _pages.ActivePage(); + auto attr = page.Attributes(); attr.SetHyperlinkId(0); - textBuffer.SetCurrentAttributes(attr); + page.SetAttributes(attr); return true; } @@ -3657,7 +3739,7 @@ bool AdaptDispatch::DoConEmuAction(const std::wstring_view string) // This seems like basically the same as 133;B - the end of the prompt, the start of the commandline. else if (subParam == 12) { - _api.GetTextBuffer().StartCommand(); + _pages.ActivePage().Buffer().StartCommand(); return true; } @@ -3701,7 +3783,7 @@ bool AdaptDispatch::DoITerm2Action(const std::wstring_view string) bool handled = false; if (action == L"SetMark") { - _api.GetTextBuffer().StartPrompt(); + _pages.ActivePage().Buffer().StartPrompt(); handled = true; } @@ -3747,19 +3829,19 @@ bool AdaptDispatch::DoFinalTermAction(const std::wstring_view string) { case L'A': // FTCS_PROMPT { - _api.GetTextBuffer().StartPrompt(); + _pages.ActivePage().Buffer().StartPrompt(); handled = true; break; } case L'B': // FTCS_COMMAND_START { - _api.GetTextBuffer().StartCommand(); + _pages.ActivePage().Buffer().StartCommand(); handled = true; break; } case L'C': // FTCS_COMMAND_EXECUTED { - _api.GetTextBuffer().StartOutput(); + _pages.ActivePage().Buffer().StartOutput(); handled = true; break; } @@ -3780,7 +3862,7 @@ bool AdaptDispatch::DoFinalTermAction(const std::wstring_view string) UINT_MAX; } - _api.GetTextBuffer().EndCurrentCommand(error); + _pages.ActivePage().Buffer().EndCurrentCommand(error); handled = true; break; @@ -4270,7 +4352,7 @@ void AdaptDispatch::_ReportSGRSetting() const fmt::basic_memory_buffer response; response.append(L"\033P1$r0"sv); - const auto& attr = _api.GetTextBuffer().GetCurrentAttributes(); + const auto& attr = _pages.ActivePage().Attributes(); const auto ulStyle = attr.GetUnderlineStyle(); // For each boolean attribute that is set, we add the appropriate // parameter value to the response string. @@ -4339,8 +4421,8 @@ void AdaptDispatch::_ReportDECSTBMSetting() fmt::basic_memory_buffer response; response.append(L"\033P1$r"sv); - const auto viewport = _api.GetViewport(); - const auto [marginTop, marginBottom] = _GetVerticalMargins(viewport, false); + const auto page = _pages.ActivePage(); + const auto [marginTop, marginBottom] = _GetVerticalMargins(page, false); // VT origin is at 1,1 so we need to add 1 to these margins. fmt::format_to(std::back_inserter(response), FMT_COMPILE(L"{};{}"), marginTop + 1, marginBottom + 1); @@ -4363,8 +4445,8 @@ void AdaptDispatch::_ReportDECSLRMSetting() fmt::basic_memory_buffer response; response.append(L"\033P1$r"sv); - const auto bufferWidth = _api.GetTextBuffer().GetSize().Width(); - const auto [marginLeft, marginRight] = _GetHorizontalMargins(bufferWidth); + const auto pageWidth = _pages.ActivePage().Width(); + const auto [marginLeft, marginRight] = _GetHorizontalMargins(pageWidth); // VT origin is at 1,1 so we need to add 1 to these margins. fmt::format_to(std::back_inserter(response), FMT_COMPILE(L"{};{}"), marginLeft + 1, marginRight + 1); @@ -4387,7 +4469,7 @@ void AdaptDispatch::_ReportDECSCASetting() const fmt::basic_memory_buffer response; response.append(L"\033P1$r"sv); - const auto& attr = _api.GetTextBuffer().GetCurrentAttributes(); + const auto& attr = _pages.ActivePage().Attributes(); response.append(attr.IsProtected() ? L"1"sv : L"0"sv); // The '"q' indicates this is an DECSCA response, and ST ends the sequence. @@ -4505,31 +4587,27 @@ ITermDispatch::StringHandler AdaptDispatch::RestorePresentationState(const Dispa // - None void AdaptDispatch::_ReportCursorInformation() { - const auto viewport = _api.GetViewport(); - const auto& textBuffer = _api.GetTextBuffer(); - const auto& cursor = textBuffer.GetCursor(); - const auto& attributes = textBuffer.GetCurrentAttributes(); + const auto page = _pages.ActivePage(); + const auto& cursor = page.Cursor(); + const auto& attributes = page.Attributes(); // First pull the cursor position relative to the entire buffer out of the console. til::point cursorPosition{ cursor.GetPosition() }; - // Now adjust it for its position in respect to the current viewport top. - cursorPosition.y -= viewport.top; + // Now adjust it for its position in respect to the current page top. + cursorPosition.y -= page.Top(); - // NOTE: 1,1 is the top-left corner of the viewport in VT-speak, so add 1. + // NOTE: 1,1 is the top-left corner of the page in VT-speak, so add 1. cursorPosition.x++; cursorPosition.y++; // If the origin mode is set, the cursor is relative to the margin origin. if (_modes.test(Mode::Origin)) { - cursorPosition.x -= _GetHorizontalMargins(textBuffer.GetSize().Width()).first; - cursorPosition.y -= _GetVerticalMargins(viewport, false).first; + cursorPosition.x -= _GetHorizontalMargins(page.Width()).first; + cursorPosition.y -= _GetVerticalMargins(page, false).first; } - // Paging is not supported yet (GH#13892). - const auto pageNumber = 1; - // Only some of the rendition attributes are reported. // Bit Attribute // 1 bold @@ -4575,7 +4653,7 @@ void AdaptDispatch::_ReportCursorInformation() FMT_COMPILE(L"\033P1$u{};{};{};{};{};{};{};{};{};{}{}{}{}\033\\"), cursorPosition.y, cursorPosition.x, - pageNumber, + page.Number(), renditionAttributes, characterAttributes, flags, @@ -4612,7 +4690,6 @@ ITermDispatch::StringHandler AdaptDispatch::_RestoreCursorInformation() VTParameter row{}; VTParameter column{}; }; - auto& textBuffer = _api.GetTextBuffer(); return [&, state = State{}](const auto ch) mutable { if (numeric.test(state.field)) { @@ -4634,7 +4711,7 @@ ITermDispatch::StringHandler AdaptDispatch::_RestoreCursorInformation() } else if (state.field == Field::Page) { - // Paging is not supported yet (GH#13892). + PagePositionAbsolute(state.value); } else if (state.field == Field::GL && state.value <= 3) { @@ -4659,19 +4736,21 @@ ITermDispatch::StringHandler AdaptDispatch::_RestoreCursorInformation() state.value = ch; if (state.field == Field::SGR) { - auto attr = textBuffer.GetCurrentAttributes(); + const auto page = _pages.ActivePage(); + auto attr = page.Attributes(); attr.SetIntense(state.value & 1); attr.SetUnderlineStyle(state.value & 2 ? UnderlineStyle::SinglyUnderlined : UnderlineStyle::NoUnderline); attr.SetBlinking(state.value & 4); attr.SetReverseVideo(state.value & 8); attr.SetInvisible(state.value & 16); - textBuffer.SetCurrentAttributes(attr); + page.SetAttributes(attr); } else if (state.field == Field::Attr) { - auto attr = textBuffer.GetCurrentAttributes(); + const auto page = _pages.ActivePage(); + auto attr = page.Attributes(); attr.SetProtected(state.value & 1); - textBuffer.SetCurrentAttributes(attr); + page.SetAttributes(attr); } else if (state.field == Field::Sizes) { @@ -4697,7 +4776,8 @@ ITermDispatch::StringHandler AdaptDispatch::_RestoreCursorInformation() // above, so we only need to worry about setting it. if (delayedEOLWrap) { - textBuffer.GetCursor().DelayEOLWrap(); + const auto page = _pages.ActivePage(); + page.Cursor().DelayEOLWrap(); } } } @@ -4744,7 +4824,7 @@ void AdaptDispatch::_ReportTabStops() // In order to be compatible with the original hardware terminals, we only // report tab stops up to the current buffer width, even though there may // be positions recorded beyond that limit. - const auto width = _api.GetTextBuffer().GetSize().Dimensions().width; + const auto width = _pages.ActivePage().Width(); _InitTabStopsForWidth(width); using namespace std::string_view_literals; @@ -4780,7 +4860,7 @@ ITermDispatch::StringHandler AdaptDispatch::_RestoreTabStops() // In order to be compatible with the original hardware terminals, we need // to be able to set tab stops up to at least 132 columns, even though the // current buffer width may be less than that. - const auto width = std::max(_api.GetTextBuffer().GetSize().Dimensions().width, 132); + const auto width = std::max(_pages.ActivePage().Width(), 132); _ClearAllTabStops(); _InitTabStopsForWidth(width); diff --git a/src/terminal/adapter/adaptDispatch.hpp b/src/terminal/adapter/adaptDispatch.hpp index 36bb0312145..dbea2fcd887 100644 --- a/src/terminal/adapter/adaptDispatch.hpp +++ b/src/terminal/adapter/adaptDispatch.hpp @@ -18,6 +18,7 @@ Author(s): #include "ITerminalApi.hpp" #include "FontBuffer.hpp" #include "MacroBuffer.hpp" +#include "PageManager.hpp" #include "terminalOutput.hpp" #include "../input/terminalInput.hpp" #include "../../types/inc/sgrStack.hpp" @@ -81,6 +82,12 @@ namespace Microsoft::Console::VirtualTerminal bool RequestTerminalParameters(const DispatchTypes::ReportingPermission permission) override; // DECREQTPARM bool ScrollUp(const VTInt distance) override; // SU bool ScrollDown(const VTInt distance) override; // SD + bool NextPage(const VTInt pageCount) override; // NP + bool PrecedingPage(const VTInt pageCount) override; // PP + bool PagePositionAbsolute(const VTInt page) override; // PPA + bool PagePositionRelative(const VTInt pageCount) override; // PPR + bool PagePositionBack(const VTInt pageCount) override; // PPB + bool RequestDisplayedExtent() override; // DECRQDE bool InsertLine(const VTInt distance) override; // IL bool DeleteLine(const VTInt distance) override; // DL bool InsertColumn(const VTInt distance) override; // DECIC @@ -178,7 +185,8 @@ namespace Microsoft::Console::VirtualTerminal AllowDECCOLM, AllowDECSLRM, EraseColor, - RectangularChangeExtent + RectangularChangeExtent, + PageCursorCoupling }; enum class ScrollDirection { @@ -189,6 +197,7 @@ namespace Microsoft::Console::VirtualTerminal { VTInt Row = 1; VTInt Column = 1; + VTInt Page = 1; bool IsDelayedEOLWrap = false; bool IsOriginModeRelative = false; TextAttribute Attributes = {}; @@ -214,20 +223,20 @@ namespace Microsoft::Console::VirtualTerminal }; void _WriteToBuffer(const std::wstring_view string); - std::pair _GetVerticalMargins(const til::rect& viewport, const bool absolute) noexcept; + std::pair _GetVerticalMargins(const Page& page, const bool absolute) noexcept; std::pair _GetHorizontalMargins(const til::CoordType bufferWidth) noexcept; bool _CursorMovePosition(const Offset rowOffset, const Offset colOffset, const bool clampInMargins); void _ApplyCursorMovementFlags(Cursor& cursor) noexcept; - void _FillRect(TextBuffer& textBuffer, const til::rect& fillRect, const std::wstring_view& fillChar, const TextAttribute& fillAttrs) const; - void _SelectiveEraseRect(TextBuffer& textBuffer, const til::rect& eraseRect); - void _ChangeRectAttributes(TextBuffer& textBuffer, const til::rect& changeRect, const ChangeOps& changeOps); + void _FillRect(const Page& page, const til::rect& fillRect, const std::wstring_view& fillChar, const TextAttribute& fillAttrs) const; + void _SelectiveEraseRect(const Page& page, const til::rect& eraseRect); + void _ChangeRectAttributes(const Page& page, const til::rect& changeRect, const ChangeOps& changeOps); void _ChangeRectOrStreamAttributes(const til::rect& changeArea, const ChangeOps& changeOps); - til::rect _CalculateRectArea(const VTInt top, const VTInt left, const VTInt bottom, const VTInt right, const til::size bufferSize); + til::rect _CalculateRectArea(const Page& page, const VTInt top, const VTInt left, const VTInt bottom, const VTInt right); bool _EraseScrollback(); bool _EraseAll(); - TextAttribute _GetEraseAttributes(const TextBuffer& textBuffer) const noexcept; - void _ScrollRectVertically(TextBuffer& textBuffer, const til::rect& scrollRect, const VTInt delta); - void _ScrollRectHorizontally(TextBuffer& textBuffer, const til::rect& scrollRect, const VTInt delta); + TextAttribute _GetEraseAttributes(const Page& page) const noexcept; + void _ScrollRectVertically(const Page& page, const til::rect& scrollRect, const VTInt delta); + void _ScrollRectHorizontally(const Page& page, const til::rect& scrollRect, const VTInt delta); void _InsertDeleteCharacterHelper(const VTInt delta); void _InsertDeleteLineHelper(const VTInt delta); void _InsertDeleteColumnHelper(const VTInt delta); @@ -240,7 +249,7 @@ namespace Microsoft::Console::VirtualTerminal const VTInt rightMargin, const bool homeCursor = false); - void _DoLineFeed(TextBuffer& textBuffer, const bool withReturn, const bool wrapForced); + void _DoLineFeed(const Page& page, const bool withReturn, const bool wrapForced); void _DeviceStatusReport(const wchar_t* parameters) const; void _CursorPositionReport(const bool extendedReport); @@ -281,6 +290,7 @@ namespace Microsoft::Console::VirtualTerminal RenderSettings& _renderSettings; TerminalInput& _terminalInput; TerminalOutput _termOutput; + PageManager _pages; std::unique_ptr _fontBuffer; std::shared_ptr _macroBuffer; std::optional _initialCodePage; @@ -295,7 +305,7 @@ namespace Microsoft::Console::VirtualTerminal til::inclusive_rect _scrollMargins; - til::enumset _modes; + til::enumset _modes{ Mode::PageCursorCoupling }; SgrStack _sgrStack; diff --git a/src/terminal/adapter/adaptDispatchGraphics.cpp b/src/terminal/adapter/adaptDispatchGraphics.cpp index ac1b0180eff..f7dab41cd1c 100644 --- a/src/terminal/adapter/adaptDispatchGraphics.cpp +++ b/src/terminal/adapter/adaptDispatchGraphics.cpp @@ -422,9 +422,10 @@ void AdaptDispatch::_ApplyGraphicsOptions(const VTParameters options, // - True. bool AdaptDispatch::SetGraphicsRendition(const VTParameters options) { - auto attr = _api.GetTextBuffer().GetCurrentAttributes(); + const auto page = _pages.ActivePage(); + auto attr = page.Attributes(); _ApplyGraphicsOptions(options, attr); - _api.SetTextAttributes(attr); + page.SetAttributes(attr, &_api); return true; } @@ -438,8 +439,8 @@ bool AdaptDispatch::SetGraphicsRendition(const VTParameters options) // - True. bool AdaptDispatch::SetCharacterProtectionAttribute(const VTParameters options) { - auto& textBuffer = _api.GetTextBuffer(); - auto attr = textBuffer.GetCurrentAttributes(); + const auto page = _pages.ActivePage(); + auto attr = page.Attributes(); for (size_t i = 0; i < options.size(); i++) { const LogicalAttributeOptions opt = options.at(i); @@ -456,7 +457,7 @@ bool AdaptDispatch::SetCharacterProtectionAttribute(const VTParameters options) break; } } - textBuffer.SetCurrentAttributes(attr); + page.SetAttributes(attr); return true; } @@ -470,7 +471,7 @@ bool AdaptDispatch::SetCharacterProtectionAttribute(const VTParameters options) // - True. bool AdaptDispatch::PushGraphicsRendition(const VTParameters options) { - const auto& currentAttributes = _api.GetTextBuffer().GetCurrentAttributes(); + const auto& currentAttributes = _pages.ActivePage().Attributes(); _sgrStack.Push(currentAttributes, options); return true; } @@ -484,7 +485,8 @@ bool AdaptDispatch::PushGraphicsRendition(const VTParameters options) // - True. bool AdaptDispatch::PopGraphicsRendition() { - const auto& currentAttributes = _api.GetTextBuffer().GetCurrentAttributes(); - _api.SetTextAttributes(_sgrStack.Pop(currentAttributes)); + const auto page = _pages.ActivePage(); + const auto& currentAttributes = page.Attributes(); + page.SetAttributes(_sgrStack.Pop(currentAttributes), &_api); return true; } diff --git a/src/terminal/adapter/lib/adapter.vcxproj b/src/terminal/adapter/lib/adapter.vcxproj index 2780c818dfa..a05c8e5b83e 100644 --- a/src/terminal/adapter/lib/adapter.vcxproj +++ b/src/terminal/adapter/lib/adapter.vcxproj @@ -15,6 +15,7 @@ + @@ -29,6 +30,7 @@ + diff --git a/src/terminal/adapter/lib/adapter.vcxproj.filters b/src/terminal/adapter/lib/adapter.vcxproj.filters index 798522c651f..501f6ce9bdc 100644 --- a/src/terminal/adapter/lib/adapter.vcxproj.filters +++ b/src/terminal/adapter/lib/adapter.vcxproj.filters @@ -36,6 +36,9 @@ Source Files + + Source Files + @@ -74,6 +77,9 @@ Header Files + + Header Files + diff --git a/src/terminal/adapter/sources.inc b/src/terminal/adapter/sources.inc index a7a07a29814..3ffd6bed7c7 100644 --- a/src/terminal/adapter/sources.inc +++ b/src/terminal/adapter/sources.inc @@ -34,6 +34,7 @@ SOURCES= \ ..\FontBuffer.cpp \ ..\InteractDispatch.cpp \ ..\MacroBuffer.cpp \ + ..\PageManager.cpp \ ..\adaptDispatchGraphics.cpp \ ..\terminalOutput.cpp \ diff --git a/src/terminal/adapter/termDispatch.hpp b/src/terminal/adapter/termDispatch.hpp index 82bb4f21320..3a1d19a0964 100644 --- a/src/terminal/adapter/termDispatch.hpp +++ b/src/terminal/adapter/termDispatch.hpp @@ -42,6 +42,12 @@ class Microsoft::Console::VirtualTerminal::TermDispatch : public Microsoft::Cons bool DeleteCharacter(const VTInt /*count*/) override { return false; } // DCH bool ScrollUp(const VTInt /*distance*/) override { return false; } // SU bool ScrollDown(const VTInt /*distance*/) override { return false; } // SD + bool NextPage(const VTInt /*pageCount*/) override { return false; } // NP + bool PrecedingPage(const VTInt /*pageCount*/) override { return false; } // PP + bool PagePositionAbsolute(const VTInt /*page*/) override { return false; } // PPA + bool PagePositionRelative(const VTInt /*pageCount*/) override { return false; } // PPR + bool PagePositionBack(const VTInt /*pageCount*/) override { return false; } // PPB + bool RequestDisplayedExtent() override { return false; } // DECRQDE bool InsertLine(const VTInt /*distance*/) override { return false; } // IL bool DeleteLine(const VTInt /*distance*/) override { return false; } // DL bool InsertColumn(const VTInt /*distance*/) override { return false; } // DECIC diff --git a/src/terminal/adapter/ut_adapter/adapterTest.cpp b/src/terminal/adapter/ut_adapter/adapterTest.cpp index 5cd5fbdc29e..c0ff50e5748 100644 --- a/src/terminal/adapter/ut_adapter/adapterTest.cpp +++ b/src/terminal/adapter/ut_adapter/adapterTest.cpp @@ -81,14 +81,10 @@ class TestGetSet final : public ITerminalApi return *_stateMachine; } - TextBuffer& GetTextBuffer() override + BufferState GetBufferAndViewport() override { - return *_textBuffer.get(); - } - - til::rect GetViewport() const override - { - return { _viewport.left, _viewport.top, _viewport.right, _viewport.bottom }; + const auto viewport = til::rect{ _viewport.left, _viewport.top, _viewport.right, _viewport.bottom }; + return { *_textBuffer.get(), viewport, true }; } void SetViewportPosition(const til::point /*position*/) override @@ -1575,14 +1571,23 @@ class AdapterTest coordCursorExpected.x++; coordCursorExpected.y++; - // Until we support paging (GH#13892) the reported page number should always be 1. - const auto pageExpected = 1; + // By default, the initial page number should be 1. + auto pageExpected = 1; VERIFY_IS_TRUE(_pDispatch->DeviceStatusReport(DispatchTypes::StatusType::ExtendedCursorPositionReport, {})); wchar_t pwszBuffer[50]; swprintf_s(pwszBuffer, ARRAYSIZE(pwszBuffer), L"\x1b[?%d;%d;%dR", coordCursorExpected.y, coordCursorExpected.x, pageExpected); _testGetSet->ValidateInputEvent(pwszBuffer); + + // Now test with the page number set to 3. + pageExpected = 3; + _pDispatch->PagePositionAbsolute(pageExpected); + + VERIFY_IS_TRUE(_pDispatch->DeviceStatusReport(DispatchTypes::StatusType::ExtendedCursorPositionReport, {})); + + swprintf_s(pwszBuffer, ARRAYSIZE(pwszBuffer), L"\x1b[?%d;%d;%dR", coordCursorExpected.y, coordCursorExpected.x, pageExpected); + _testGetSet->ValidateInputEvent(pwszBuffer); } TEST_METHOD(DeviceStatus_MacroSpaceReportTest) @@ -1746,6 +1751,42 @@ class AdapterTest VERIFY_THROWS(_pDispatch->TertiaryDeviceAttributes(), std::exception); } + TEST_METHOD(RequestDisplayedExtentTests) + { + Log::Comment(L"Starting test..."); + + Log::Comment(L"Test 1: Verify DECRQDE response in home position"); + _testGetSet->PrepData(); + _testGetSet->_viewport.left = 0; + _testGetSet->_viewport.right = 80; + _testGetSet->_viewport.top = 0; + _testGetSet->_viewport.bottom = 24; + VERIFY_IS_TRUE(_pDispatch->RequestDisplayedExtent()); + _testGetSet->ValidateInputEvent(L"\x1b[24;80;1;1;1\"w"); + + Log::Comment(L"Test 2: Verify DECRQDE response when panned horizontally"); + _testGetSet->_viewport.left += 5; + _testGetSet->_viewport.right += 5; + VERIFY_IS_TRUE(_pDispatch->RequestDisplayedExtent()); + _testGetSet->ValidateInputEvent(L"\x1b[24;80;6;1;1\"w"); + + Log::Comment(L"Test 3: Verify DECRQDE response on page 3"); + _pDispatch->PagePositionAbsolute(3); + VERIFY_IS_TRUE(_pDispatch->RequestDisplayedExtent()); + _testGetSet->ValidateInputEvent(L"\x1b[24;80;6;1;3\"w"); + + Log::Comment(L"Test 3: Verify DECRQDE response when active page not visible"); + _pDispatch->ResetMode(DispatchTypes::ModeParams::DECPCCM_PageCursorCouplingMode); + _pDispatch->PagePositionAbsolute(1); + VERIFY_IS_TRUE(_pDispatch->RequestDisplayedExtent()); + _testGetSet->ValidateInputEvent(L"\x1b[24;80;6;1;3\"w"); + + Log::Comment(L"Test 4: Verify DECRQDE response when page 1 visible again"); + _pDispatch->SetMode(DispatchTypes::ModeParams::DECPCCM_PageCursorCouplingMode); + VERIFY_IS_TRUE(_pDispatch->RequestDisplayedExtent()); + _testGetSet->ValidateInputEvent(L"\x1b[24;80;6;1;1\"w"); + } + TEST_METHOD(RequestTerminalParametersTests) { Log::Comment(L"Starting test..."); @@ -3263,7 +3304,7 @@ class AdapterTest setMacroText(63, L"Macro 63"); const auto getBufferOutput = [&]() { - const auto& textBuffer = _testGetSet->GetTextBuffer(); + const auto& textBuffer = _testGetSet->GetBufferAndViewport().buffer; const auto cursorPos = textBuffer.GetCursor().GetPosition(); return textBuffer.GetRowByOffset(cursorPos.y).GetText().substr(0, cursorPos.x); }; @@ -3314,7 +3355,8 @@ class AdapterTest { _testGetSet->PrepData(); _pDispatch->WindowManipulation(DispatchTypes::WindowManipulationType::ReportTextSizeInCharacters, NULL, NULL); - const std::wstring expectedResponse = fmt::format(L"\033[8;{};{}t", _testGetSet->GetViewport().height(), _testGetSet->GetTextBuffer().GetSize().Width()); + const auto [textBuffer, viewport, _] = _testGetSet->GetBufferAndViewport(); + const std::wstring expectedResponse = fmt::format(L"\033[8;{};{}t", viewport.height(), textBuffer.GetSize().Width()); _testGetSet->ValidateInputEvent(expectedResponse.c_str()); } @@ -3345,6 +3387,89 @@ class AdapterTest VERIFY_IS_TRUE(_pDispatch->DoVsCodeAction(LR"(Completions;10;20;30;{ "foo": "what;ever", "bar": 2 })")); } + TEST_METHOD(PageMovementTests) + { + _testGetSet->PrepData(CursorX::XCENTER, CursorY::YCENTER); + auto& pages = _pDispatch->_pages; + const auto startPos = pages.ActivePage().Cursor().GetPosition(); + const auto homePos = til::point{ 0, pages.ActivePage().Top() }; + + // Testing PPA (page position absolute) + VERIFY_ARE_EQUAL(1, pages.ActivePage().Number(), L"Initial page is 1"); + _pDispatch->PagePositionAbsolute(3); + VERIFY_ARE_EQUAL(3, pages.ActivePage().Number(), L"PPA 3 moves to page 3"); + _pDispatch->PagePositionAbsolute(VTParameter{}); + VERIFY_ARE_EQUAL(1, pages.ActivePage().Number(), L"PPA with omitted page moves to 1"); + _pDispatch->PagePositionAbsolute(9999); + VERIFY_ARE_EQUAL(6, pages.ActivePage().Number(), L"PPA is clamped at page 6"); + VERIFY_ARE_EQUAL(startPos, pages.ActivePage().Cursor().GetPosition(), L"Cursor position never changes"); + + _testGetSet->PrepData(CursorX::XCENTER, CursorY::YCENTER); + _pDispatch->PagePositionAbsolute(1); // Reset to page 1 + + // Testing PPR (page position relative) + VERIFY_ARE_EQUAL(1, pages.ActivePage().Number(), L"Initial page is 1"); + _pDispatch->PagePositionRelative(2); + VERIFY_ARE_EQUAL(3, pages.ActivePage().Number(), L"PPR 2 moves forward 2 pages"); + _pDispatch->PagePositionRelative(VTParameter{}); + VERIFY_ARE_EQUAL(4, pages.ActivePage().Number(), L"PPR with omitted count moves forward 1"); + _pDispatch->PagePositionRelative(9999); + VERIFY_ARE_EQUAL(6, pages.ActivePage().Number(), L"PPR is clamped at page 6"); + VERIFY_ARE_EQUAL(startPos, pages.ActivePage().Cursor().GetPosition(), L"Cursor position never changes"); + + _testGetSet->PrepData(CursorX::XCENTER, CursorY::YCENTER); + + // Testing PPB (page position back) + VERIFY_ARE_EQUAL(6, pages.ActivePage().Number(), L"Initial page is 6"); + _pDispatch->PagePositionBack(2); + VERIFY_ARE_EQUAL(4, pages.ActivePage().Number(), L"PPB 2 moves back 2 pages"); + _pDispatch->PagePositionBack(VTParameter{}); + VERIFY_ARE_EQUAL(3, pages.ActivePage().Number(), L"PPB with omitted count moves back 1"); + _pDispatch->PagePositionBack(9999); + VERIFY_ARE_EQUAL(1, pages.ActivePage().Number(), L"PPB is clamped at page 1"); + VERIFY_ARE_EQUAL(startPos, pages.ActivePage().Cursor().GetPosition(), L"Cursor position never changes"); + + _testGetSet->PrepData(CursorX::XCENTER, CursorY::YCENTER); + + // Testing NP (next page) + VERIFY_ARE_EQUAL(1, pages.ActivePage().Number(), L"Initial page is 1"); + _pDispatch->NextPage(2); + VERIFY_ARE_EQUAL(3, pages.ActivePage().Number(), L"NP 2 moves forward 2 pages"); + _pDispatch->NextPage(VTParameter{}); + VERIFY_ARE_EQUAL(4, pages.ActivePage().Number(), L"NP with omitted count moves forward 1"); + _pDispatch->NextPage(9999); + VERIFY_ARE_EQUAL(6, pages.ActivePage().Number(), L"NP is clamped at page 6"); + VERIFY_ARE_EQUAL(homePos, pages.ActivePage().Cursor().GetPosition(), L"Cursor position is reset to home"); + + _testGetSet->PrepData(CursorX::XCENTER, CursorY::YCENTER); + + // Testing PP (preceding page) + VERIFY_ARE_EQUAL(6, pages.ActivePage().Number(), L"Initial page is 6"); + _pDispatch->PrecedingPage(2); + VERIFY_ARE_EQUAL(4, pages.ActivePage().Number(), L"PP 2 moves back 2 pages"); + _pDispatch->PrecedingPage(VTParameter{}); + VERIFY_ARE_EQUAL(3, pages.ActivePage().Number(), L"PP with omitted count moves back 1"); + _pDispatch->PrecedingPage(9999); + VERIFY_ARE_EQUAL(1, pages.ActivePage().Number(), L"PP is clamped at page 1"); + VERIFY_ARE_EQUAL(homePos, pages.ActivePage().Cursor().GetPosition(), L"Cursor position is reset to home"); + + // Testing DECPCCM (page cursor coupling mode) + _pDispatch->SetMode(DispatchTypes::ModeParams::DECPCCM_PageCursorCouplingMode); + _pDispatch->PagePositionAbsolute(2); + VERIFY_ARE_EQUAL(2, pages.ActivePage().Number()); + VERIFY_ARE_EQUAL(2, pages.VisiblePage().Number(), L"Visible page should follow active if DECPCCM set"); + _pDispatch->ResetMode(DispatchTypes::ModeParams::DECPCCM_PageCursorCouplingMode); + _pDispatch->PagePositionAbsolute(4); + VERIFY_ARE_EQUAL(4, pages.ActivePage().Number()); + VERIFY_ARE_EQUAL(2, pages.VisiblePage().Number(), L"Visible page should not change if DECPCCM reset"); + _pDispatch->SetMode(DispatchTypes::ModeParams::DECPCCM_PageCursorCouplingMode); + VERIFY_ARE_EQUAL(4, pages.ActivePage().Number()); + VERIFY_ARE_EQUAL(4, pages.VisiblePage().Number(), L"Active page should become visible when DECPCCM set"); + + // Reset to page 1 + _pDispatch->PagePositionAbsolute(1); + } + private: TerminalInput _terminalInput; std::unique_ptr _testGetSet; diff --git a/src/terminal/parser/OutputStateMachineEngine.cpp b/src/terminal/parser/OutputStateMachineEngine.cpp index f4b3fcbf677..7568f154677 100644 --- a/src/terminal/parser/OutputStateMachineEngine.cpp +++ b/src/terminal/parser/OutputStateMachineEngine.cpp @@ -556,6 +556,12 @@ bool OutputStateMachineEngine::ActionCsiDispatch(const VTID id, const VTParamete case CsiActionCodes::SD_ScrollDown: success = _dispatch->ScrollDown(parameters.at(0)); break; + case CsiActionCodes::NP_NextPage: + success = _dispatch->NextPage(parameters.at(0)); + break; + case CsiActionCodes::PP_PrecedingPage: + success = _dispatch->PrecedingPage(parameters.at(0)); + break; case CsiActionCodes::ANSISYSRC_CursorRestore: success = _dispatch->CursorRestoreState(); break; @@ -601,6 +607,15 @@ bool OutputStateMachineEngine::ActionCsiDispatch(const VTID id, const VTParamete } success = true; break; + case CsiActionCodes::PPA_PagePositionAbsolute: + success = _dispatch->PagePositionAbsolute(parameters.at(0)); + break; + case CsiActionCodes::PPR_PagePositionRelative: + success = _dispatch->PagePositionRelative(parameters.at(0)); + break; + case CsiActionCodes::PPB_PagePositionBack: + success = _dispatch->PagePositionBack(parameters.at(0)); + break; case CsiActionCodes::DECSCUSR_SetCursorStyle: success = _dispatch->SetCursorStyle(parameters.at(0)); break; @@ -610,6 +625,9 @@ bool OutputStateMachineEngine::ActionCsiDispatch(const VTID id, const VTParamete case CsiActionCodes::DECSCA_SetCharacterProtectionAttribute: success = _dispatch->SetCharacterProtectionAttribute(parameters); break; + case CsiActionCodes::DECRQDE_RequestDisplayedExtent: + success = _dispatch->RequestDisplayedExtent(); + break; case CsiActionCodes::XT_PushSgr: case CsiActionCodes::XT_PushSgrAlias: success = _dispatch->PushGraphicsRendition(parameters); diff --git a/src/terminal/parser/OutputStateMachineEngine.hpp b/src/terminal/parser/OutputStateMachineEngine.hpp index 64fbf81503e..0970e45dee2 100644 --- a/src/terminal/parser/OutputStateMachineEngine.hpp +++ b/src/terminal/parser/OutputStateMachineEngine.hpp @@ -122,6 +122,8 @@ namespace Microsoft::Console::VirtualTerminal DCH_DeleteCharacter = VTID("P"), SU_ScrollUp = VTID("S"), SD_ScrollDown = VTID("T"), + NP_NextPage = VTID("U"), + PP_PrecedingPage = VTID("V"), DECST8C_SetTabEvery8Columns = VTID("?W"), ECH_EraseCharacters = VTID("X"), CBT_CursorBackTab = VTID("Z"), @@ -147,9 +149,13 @@ namespace Microsoft::Console::VirtualTerminal DTTERM_WindowManipulation = VTID("t"), // NOTE: Overlaps with DECSLPP. Fix when/if implemented. ANSISYSRC_CursorRestore = VTID("u"), DECREQTPARM_RequestTerminalParameters = VTID("x"), + PPA_PagePositionAbsolute = VTID(" P"), + PPR_PagePositionRelative = VTID(" Q"), + PPB_PagePositionBack = VTID(" R"), DECSCUSR_SetCursorStyle = VTID(" q"), DECSTR_SoftReset = VTID("!p"), DECSCA_SetCharacterProtectionAttribute = VTID("\"q"), + DECRQDE_RequestDisplayedExtent = VTID("\"v"), XT_PushSgrAlias = VTID("#p"), XT_PopSgrAlias = VTID("#q"), XT_PushSgr = VTID("#{"),