Skip to content

Commit

Permalink
Wip/mk/whitespace chars monospace font table viz (#10563)
Browse files Browse the repository at this point in the history
  • Loading branch information
marthasharkey authored Jul 23, 2024
1 parent c6251ad commit 8af3fd4
Show file tree
Hide file tree
Showing 4 changed files with 243 additions and 7 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@
- [Output component in collapsed function changed][10577]. It cannot be deleted
anymore, except by directily editing the code.
- [Multiselect drop-down widget visuals are improved][10607].
- [Text displayed in monospace and whitespace rendered as symbols][10563].

[10433]: https://github.com/enso-org/enso/pull/10443
[10457]: https://github.com/enso-org/enso/pull/10457
[10509]: https://github.com/enso-org/enso/pull/10509
[10546]: https://github.com/enso-org/enso/pull/10546
[10577]: https://github.com/enso-org/enso/pull/10577
[10607]: https://github.com/enso-org/enso/pull/10607
[10563]: https://github.com/enso-org/enso/pull/10563

#### Enso Enso Standard Library

Expand Down
111 changes: 111 additions & 0 deletions app/gui2/src/components/TextFormattingSelector.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<script setup lang="ts">
import DropdownMenu from '@/components/DropdownMenu.vue'
import MenuButton from '@/components/MenuButton.vue'
import SvgButton from '@/components/SvgButton.vue'
import SvgIcon from '@/components/SvgIcon.vue'
import { ref, watch } from 'vue'
import { TextFormatOptions } from './visualizations/TableVisualization.vue'
const emit = defineEmits<{
changeFormat: [formatValue: TextFormatOptions]
}>()
const textFormatterSelected = ref(TextFormatOptions.Partial)
watch(textFormatterSelected, (selected) => emit('changeFormat', selected))
const open = ref(false)
</script>

<template>
<DropdownMenu v-model:open="open" class="TextFormattingSelector">
<template #button><SvgIcon name="paragraph" title="Text Display Options" /> </template>

<template #entries>
<MenuButton
class="full-format"
:title="`Text displayed in monospace font and all whitespace characters displayed as symbols`"
@click="() => emit('changeFormat', TextFormatOptions.On)"
>
<SvgIcon name="paragraph" />
<div class="title">Full whitespace rendering</div>
</MenuButton>

<MenuButton
class="partial"
:title="`Text displayed in monospace font, only multiple spaces displayed with &#183;`"
@click="() => emit('changeFormat', TextFormatOptions.Partial)"
>
<SvgIcon name="paragraph" />
<div class="title">Partial whitespace rendering</div>
</MenuButton>

<MenuButton
class="off"
@click="() => emit('changeFormat', TextFormatOptions.Off)"
title="`No formatting applied to text`"
>
<div class="strikethrough">
<SvgIcon name="paragraph" />
</div>
<div class="title">No whitespace rendering</div>
</MenuButton>
</template>
</DropdownMenu>
</template>

<style scoped>
.TextFormattingSelector {
background: var(--color-frame-bg);
border-radius: 16px;
}
:deep(.DropdownMenuContent) {
margin-top: 10px;
padding: 4px;
> * {
display: flex;
padding-left: 8px;
padding-right: 8px;
}
}
.strikethrough {
position: relative;
margin-right: 4px;
}
.strikethrough:before {
position: absolute;
content: '';
left: 0;
top: 50%;
right: 0;
border-top: 1px solid;
border-color: black;
-webkit-transform: rotate(-20deg);
-moz-transform: rotate(-20deg);
-ms-transform: rotate(-20deg);
-o-transform: rotate(-20deg);
transform: rotate(-20deg);
}
.partial {
stroke: grey;
fill: #808080;
}
.off {
justify-content: flex-start;
}
.full-format {
stroke: black;
fill: #000000;
justify-content: flex-start;
}
.title {
padding-left: 2px;
}
</style>
54 changes: 48 additions & 6 deletions app/gui2/src/components/VisualizationContainer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,9 @@ const contentStyle = computed(() => {
</div>
</div>
<div v-if="$slots.toolbar && !config.isPreview" class="visualization-defined-toolbars">
<div class="toolbar"><slot name="toolbar"></slot></div>
<div class="toolbar-wrapper">
<div class="inner-toolbar"><slot name="toolbar"></slot></div>
</div>
</div>
<div
class="after-toolbars node-type"
Expand All @@ -175,7 +177,7 @@ const contentStyle = computed(() => {
<style scoped>
.VisualizationContainer {
--node-height: 32px;
--permanent-toolbar-width: 200px;
--permanent-toolbar-width: 240px;
--resize-handle-inside: var(--visualization-resize-handle-inside);
--resize-handle-outside: var(--visualization-resize-handle-outside);
--resize-handle-radius: var(--radius-default);
Expand Down Expand Up @@ -282,10 +284,28 @@ const contentStyle = computed(() => {
}
.visualization-defined-toolbars {
max-width: calc(100% - var(--permanent-toolbar-width));
/* FIXME [sb]: This will cut off floating panels - consider investigating whether there's a better
* way to clip only the toolbar div itself. */
overflow-x: hidden;
min-width: calc(100% - var(--permanent-toolbar-width));
max-width: 100%;
overflow-x: clip;
overflow-y: visible;
}
.toolbar-wrapper {
position: relative;
display: flex;
border-radius: var(--radius-full);
z-index: 20;
&:before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
border-radius: var(--radius-full);
}
}
.invisible {
Expand All @@ -303,4 +323,26 @@ const contentStyle = computed(() => {
.VisualizationContainer :deep(> .toolbars > .toolbar > *) {
position: relative;
}
.inner-toolbar {
position: relative;
display: flex;
border-radius: var(--radius-full);
gap: 12px;
padding: 8px;
z-index: 20;
&:before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
border-radius: var(--radius-full);
background: var(--color-app-bg);
backdrop-filter: var(--blur-app-bg);
}
}
</style>
83 changes: 82 additions & 1 deletion app/gui2/src/components/visualizations/TableVisualization.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
tsvTableToEnsoExpression,
writeClipboard,
} from '@/components/GraphEditor/clipboard'
import TextFormattingSelector from '@/components/TextFormattingSelector.vue'
import { Ast } from '@/util/ast'
import { Pattern } from '@/util/ast/match'
import { useAutoBlur } from '@/util/autoBlur'
Expand All @@ -16,6 +17,7 @@ import type {
CellClickedEvent,
ColumnResizedEvent,
ICellRendererParams,
RowHeightParams,
} from 'ag-grid-community'
import type { ColDef, GridOptions } from 'ag-grid-enterprise'
import {
Expand Down Expand Up @@ -90,6 +92,12 @@ interface UnknownTable {
links: string[] | undefined
}
export enum TextFormatOptions {
Partial,
On,
Off,
}
declare module 'ag-grid-enterprise' {
// These type parameters are defined on the original interface.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
Expand Down Expand Up @@ -123,6 +131,7 @@ const SQLITE_CONNECTIONS_NODE_TYPE =
'Standard.Database.Internal.SQLite.SQLite_Connection.SQLite_Connection'
const POSTGRES_CONNECTIONS_NODE_TYPE =
'Standard.Database.Internal.Postgres.Postgres_Connection.Postgres_Connection'
const DEFAULT_ROW_HEIGHT = 22
const rowLimit = ref(0)
const page = ref(0)
Expand All @@ -145,7 +154,7 @@ const defaultColDef = {
}
const agGridOptions: Ref<GridOptions & Required<Pick<GridOptions, 'defaultColDef'>>> = ref({
headerHeight: 26,
rowHeight: 22,
getRowHeight: getRowHeight,
rowData: [],
columnDefs: [],
defaultColDef: defaultColDef as typeof defaultColDef & { manuallySized: boolean },
Expand All @@ -158,6 +167,10 @@ const agGridOptions: Ref<GridOptions & Required<Pick<GridOptions, 'defaultColDef
enableRangeSelection: true,
popupParent: document.body,
})
const textFormatterSelected = ref<TextFormatOptions>(TextFormatOptions.Partial)
const updateTextFormat = (option: TextFormatOptions) => {
textFormatterSelected.value = option
}
const isRowCountSelectorVisible = computed(() => rowCount.value >= 1000)
Expand Down Expand Up @@ -242,6 +255,46 @@ function formatNumber(params: ICellRendererParams) {
return needsGrouping ? numberFormatGroupped.format(value) : numberFormat.format(value)
}
function formatText(params: ICellRendererParams) {
if (textFormatterSelected.value === TextFormatOptions.Off) {
return params.value
}
const partialMappings = {
'\r': '<span style="color: lightgrey">␍</span> <br>',
'\n': '<span style="color: lightgrey;">␊</span> <br>',
'\t': '<span style="white-space: break-spaces;"> </span>',
}
const fullMappings = {
'\r': '<span style="color: lightgrey">␍</span> <br>',
'\n': '<span style="color: lightgrey">␊</span> <br>',
'\t': '<span style="color: lightgrey; white-space: break-spaces;">&#8594; </span>',
}
const replaceSpaces =
textFormatterSelected.value === TextFormatOptions.On ?
params.value.replaceAll(' ', '<span style="color: lightgrey">&#183;</span>')
: params.value.replace(/ {2,}/g, function (match: string) {
return `<span style="color: lightgrey">${'&#183;'.repeat(match.length)}</span>`
})
const replaceReturns = replaceSpaces.replace(
/\r\n/g,
'<span style="color: lightgrey">␍␊</span> <br>',
)
const renderOtherWhitespace = (match: string) => {
return textFormatterSelected.value === TextFormatOptions.On && match != ' ' ?
'<span style="color: lightgrey">&#9744;</span>'
: match
}
const newString = replaceReturns.replace(/[\s]/g, function (match: string) {
const mapping =
textFormatterSelected.value === TextFormatOptions.On ? fullMappings : partialMappings
return mapping[match as keyof typeof mapping] || renderOtherWhitespace(match)
})
return `<span > ${newString} <span>`
}
function setRowLimit(newRowLimit: number) {
if (newRowLimit !== rowLimit.value) {
rowLimit.value = newRowLimit
Expand All @@ -265,6 +318,29 @@ function escapeHTML(str: string) {
return str.replace(/[&<>"']/g, (m) => mapping[m]!)
}
function getRowHeight(params: RowHeightParams): number {
if (textFormatterSelected.value === TextFormatOptions.Off) {
return DEFAULT_ROW_HEIGHT
}
const rowData = Object.values(params.data)
const textValues = rowData.filter((r): r is string => typeof r === 'string')
if (!textValues.length) {
return DEFAULT_ROW_HEIGHT
}
const returnCharsCount = textValues.map((text: string) => {
const crlfCount = (text.match(/\r\n/g) || []).length
const crCount = (text.match(/\r/g) || []).length
const lfCount = (text.match(/\n/g) || []).length
return crCount + lfCount - crlfCount
})
const maxReturnCharsCount = Math.max(...returnCharsCount)
return (maxReturnCharsCount + 1) * DEFAULT_ROW_HEIGHT
}
function cellClass(params: CellClassParams) {
if (params.colDef.field === '#') return null
if (typeof params.value === 'number' || params.value === null) return 'ag-right-aligned-cell'
Expand All @@ -282,6 +358,7 @@ function cellRenderer(params: ICellRendererParams) {
else if (params.value === undefined) return ''
else if (params.value === '') return '<span style="color:grey; font-style: italic;">Empty</span>'
else if (typeof params.value === 'number') return formatNumber(params)
else if (typeof params.value === 'string') return formatText(params)
else if (Array.isArray(params.value)) return `[Vector ${params.value.length} items]`
else if (typeof params.value === 'object') {
const valueType = params.value?.type
Expand Down Expand Up @@ -643,6 +720,9 @@ onUnmounted(() => {

<template>
<VisualizationContainer :belowToolbar="true" :overflow="true">
<template #toolbar>
<TextFormattingSelector @changeFormat="(i) => updateTextFormat(i)" />
</template>
<div ref="rootNode" class="TableVisualization" @wheel.stop @pointerdown.stop>
<div class="table-visualization-status-bar">
<select
Expand Down Expand Up @@ -683,6 +763,7 @@ onUnmounted(() => {
--ag-grid-size: 3px;
--ag-list-item-height: 20px;
flex-grow: 1;
font-family: monospace;
}
.table-visualization-status-bar {
Expand Down

0 comments on commit 8af3fd4

Please sign in to comment.