From ae2acf3bd7115bbc3603769181ac0db7be527169 Mon Sep 17 00:00:00 2001 From: Aaron Bockover Date: Mon, 14 May 2018 11:11:38 -0400 Subject: [PATCH 1/7] InteractiveJsonSerializerSettings: only bind to [JsonObject] types --- .../Xamarin.Interactive/Logging/LogEntry.cs | 5 +- .../Protocol/AgentFeaturesRequest.cs | 5 ++ .../EvaluationContextInitializeRequest.cs | 5 +- .../InteractiveJsonSerializerSettings.cs | 53 +++++++++++++++---- .../Program.cs | 12 +++++ docs/Xamarin.Interactive.api.cs | 2 +- 6 files changed, 68 insertions(+), 14 deletions(-) diff --git a/Agents/Xamarin.Interactive/Logging/LogEntry.cs b/Agents/Xamarin.Interactive/Logging/LogEntry.cs index d5b87d30f..734db060c 100644 --- a/Agents/Xamarin.Interactive/Logging/LogEntry.cs +++ b/Agents/Xamarin.Interactive/Logging/LogEntry.cs @@ -7,9 +7,11 @@ using System; +using Newtonsoft.Json; + namespace Xamarin.Interactive.Logging { - [Serializable] + [JsonObject] public struct LogEntry { internal string OwnerId { get; } @@ -23,6 +25,7 @@ public struct LogEntry public string CallerFilePath { get; } public int CallerLineNumber { get; } + [JsonConstructor] internal LogEntry ( string ownerId, DateTime time, diff --git a/Agents/Xamarin.Interactive/Protocol/AgentFeaturesRequest.cs b/Agents/Xamarin.Interactive/Protocol/AgentFeaturesRequest.cs index 0a1ee9324..1b6e0cdb8 100644 --- a/Agents/Xamarin.Interactive/Protocol/AgentFeaturesRequest.cs +++ b/Agents/Xamarin.Interactive/Protocol/AgentFeaturesRequest.cs @@ -14,6 +14,11 @@ namespace Xamarin.Interactive.Protocol [JsonObject] sealed class AgentFeaturesRequest : MainThreadRequest { + [JsonConstructor] + public AgentFeaturesRequest () + { + } + protected override Task HandleAsync (Agent agent) => Task.FromResult(new AgentFeatures (agent .ViewHierarchyHandlerManager diff --git a/Agents/Xamarin.Interactive/Protocol/EvaluationContextInitializeRequest.cs b/Agents/Xamarin.Interactive/Protocol/EvaluationContextInitializeRequest.cs index c2de8b230..1dd85acc1 100644 --- a/Agents/Xamarin.Interactive/Protocol/EvaluationContextInitializeRequest.cs +++ b/Agents/Xamarin.Interactive/Protocol/EvaluationContextInitializeRequest.cs @@ -4,16 +4,19 @@ using System; using System.Threading.Tasks; +using Newtonsoft.Json; + using Xamarin.Interactive.CodeAnalysis; using Xamarin.Interactive.Core; namespace Xamarin.Interactive.Protocol { - [Serializable] + [JsonObject] sealed class EvaluationContextInitializeRequest : MainThreadRequest { public TargetCompilationConfiguration Configuration { get; } + [JsonConstructor] public EvaluationContextInitializeRequest (TargetCompilationConfiguration configuration) => Configuration = configuration ?? throw new ArgumentNullException (nameof (configuration)); diff --git a/Agents/Xamarin.Interactive/Serialization/InteractiveJsonSerializerSettings.cs b/Agents/Xamarin.Interactive/Serialization/InteractiveJsonSerializerSettings.cs index c1bae3197..e35f69526 100644 --- a/Agents/Xamarin.Interactive/Serialization/InteractiveJsonSerializerSettings.cs +++ b/Agents/Xamarin.Interactive/Serialization/InteractiveJsonSerializerSettings.cs @@ -46,15 +46,6 @@ public InteractiveJsonContractResolver () }; } - static CustomAttributeData GetCustomAttributeNamed (MemberInfo member, string name) - => member.CustomAttributes.FirstOrDefault (a => a.AttributeType.Name == name); - - static ConstructorInfo GetAttributeConstructor (Type objectType) - => objectType - .GetConstructors (BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) - .Where (c => GetCustomAttributeNamed (c, "JsonConstructorAttribute") != null) - .SingleOrDefault (); - #if EXTERNAL_INTERACTIVE_JSON_SERIALIZER_SETTINGS protected override JsonObjectContract CreateObjectContract (Type objectType) @@ -62,7 +53,7 @@ protected override JsonObjectContract CreateObjectContract (Type objectType) var contract = base.CreateObjectContract (objectType); if (contract.OverrideCreator == null && !objectType.IsAbstract && !objectType.IsInterface) { - var ctor = GetAttributeConstructor (objectType); + var ctor = InteractiveJsonBinder.GetJsonConstructor (objectType); if (ctor != null) { contract.OverrideCreator = args => ctor.Invoke (args); contract.CreatorParameters.Clear (); @@ -89,6 +80,9 @@ protected override JsonProperty CreateProperty ( sealed class InteractiveJsonBinder : ISerializationBinder { + readonly Dictionary<(string assemblyName, string typeName), Type> bindToTypeCache + = new Dictionary<(string, string), Type> (); + public void BindToName (Type serializedType, out string assemblyName, out string typeName) { assemblyName = null; @@ -100,7 +94,44 @@ public void BindToName (Type serializedType, out string assemblyName, out string } public Type BindToType (string assemblyName, string typeName) - => RepresentedType.GetType (typeName); + { + // Bind only to types explicitly marked with [JsonObject]. + // These attributed types imply they are safe for deserialization. + + if (bindToTypeCache.TryGetValue ((assemblyName, typeName), out var type)) + return type; + + type = RepresentedType.GetType (typeName); + if (type == null) + return null; + + foreach (var customAttribute in type.CustomAttributes) { + switch (customAttribute.AttributeType.FullName) { + case "Xamarin.Interactive.Json.JsonObjectAttribute": + case "Newtonsoft.Json.JsonObjectAttribute": + if (GetJsonConstructor (type) != null) { + bindToTypeCache [(assemblyName, typeName)] = type; + return type; + } + + throw new InvalidOperationException ( + $"Not binding to '{typeName}, {assemblyName}'. " + + "It is marked [JsonObject] but does not have a [JsonConstructor]."); + } + } + + throw new InvalidOperationException ( + $"Not binding to '{typeName}, {assemblyName}'. It is not marked [JsonObject]."); + } + + static CustomAttributeData GetCustomAttributeNamed (MemberInfo member, string name) + => member.CustomAttributes.FirstOrDefault (a => a.AttributeType.Name == name); + + public static ConstructorInfo GetJsonConstructor (Type objectType) + => objectType + .GetConstructors (BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) + .Where (c => GetCustomAttributeNamed (c, "JsonConstructorAttribute") != null) + .SingleOrDefault (); } public diff --git a/Clients/Xamarin.Interactive.Client.Mac.SimChecker/Program.cs b/Clients/Xamarin.Interactive.Client.Mac.SimChecker/Program.cs index 10c9743cd..63586e0bb 100644 --- a/Clients/Xamarin.Interactive.Client.Mac.SimChecker/Program.cs +++ b/Clients/Xamarin.Interactive.Client.Mac.SimChecker/Program.cs @@ -14,6 +14,18 @@ using Xamarin.Interactive.Logging; using Xamarin.Interactive.MTouch; +// Define these for LogEntry since we do not actually need Newtonsoft.Json in simchecker. +namespace Newtonsoft.Json +{ + sealed class JsonObjectAttribute : Attribute + { + } + + sealed class JsonConstructorAttribute : Attribute + { + } +} + namespace Xamarin.Interactive.Mac.SimChecker { public class Program diff --git a/docs/Xamarin.Interactive.api.cs b/docs/Xamarin.Interactive.api.cs index 6a1e66c2c..cb5fa0a26 100644 --- a/docs/Xamarin.Interactive.api.cs +++ b/docs/Xamarin.Interactive.api.cs @@ -832,7 +832,7 @@ public static bool IsInitialized { public static void Warning (string tag, string message, Exception exception, [CallerMemberName] string callerMemberName = null, [CallerFilePath] string callerFilePath = null, [CallerLineNumber] int callerLineNumber = 0); } - [Serializable] + [JsonObject] public struct LogEntry { public string CallerFilePath { From c648ce84818811c96580196f1b1bc5d28f8fdaac Mon Sep 17 00:00:00 2001 From: Aaron Bockover Date: Thu, 24 May 2018 21:45:35 -0400 Subject: [PATCH 2/7] Web: initial work for overhauled result/representation UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is still largely a work in progress, but it’s functional enough and an improvement over what is in master currently. It brings a cleaner UI to result rendering and a much simpler approach for implementing renderers. There is a new representation selector that gets out of the way of the cell to help reduce vertical space waste. The run button is also moved into the cell itself as well. --- .../ClientApp/ResultRendererRegistry.ts | 57 ----- .../ClientApp/components/ActionButton.scss | 41 ++++ .../ClientApp/components/ActionButton.tsx | 51 +++++ .../ClientApp/components/CodeCell.tsx | 9 +- .../ClientApp/components/CodeCellView.scss | 102 ++++----- .../ClientApp/components/CodeCellView.tsx | 195 +++++++----------- .../components/ComponentPlayground.tsx | 71 ++++++- .../ClientApp/components/Dropdown.scss | 48 +++++ .../ClientApp/components/Dropdown.tsx | 142 +++++++++++++ .../components/RepresentationSelector.scss | 40 ++++ .../components/RepresentationSelector.tsx | 180 ++++++++++++++++ .../ClientApp/components/TransitionGroup.scss | 17 ++ .../ClientApp/components/TransitionGroup.tsx | 50 +++++ .../ClientApp/components/WorkbookEditor.tsx | 2 +- .../ClientApp/components/WorkbookShell.tsx | 6 +- .../ClientApp/css/theme.scss | 16 +- .../ClientApp/evaluation.ts | 1 + .../ClientApp/renderers/CalendarRenderer.tsx | 30 +-- .../CapturedOutputRenderer.scss} | 0 .../CapturedOutputRenderer.tsx} | 16 +- .../ClientApp/renderers/ImageRenderer.tsx | 64 ++---- .../renderers/InteractiveObjectRenderer.tsx | 4 + .../ClientApp/renderers/NullRenderer.tsx | 22 +- .../ClientApp/renderers/TestRenderer.tsx | 46 ----- .../ClientApp/renderers/ToStringRenderer.tsx | 58 ++---- .../renderers/TypeSystemRenderers.tsx | 55 ++--- .../renderers/VerbatimHtmlRenderer.tsx | 59 +++--- .../ClientApp/rendering.ts | 101 ++++++++- .../ClientApp/utils.ts | 64 ++++++ .../Icons.sketch | Bin 0 -> 8375 bytes 30 files changed, 1044 insertions(+), 503 deletions(-) delete mode 100644 Clients/Xamarin.Interactive.Client.Web/ClientApp/ResultRendererRegistry.ts create mode 100644 Clients/Xamarin.Interactive.Client.Web/ClientApp/components/ActionButton.scss create mode 100644 Clients/Xamarin.Interactive.Client.Web/ClientApp/components/ActionButton.tsx create mode 100644 Clients/Xamarin.Interactive.Client.Web/ClientApp/components/Dropdown.scss create mode 100644 Clients/Xamarin.Interactive.Client.Web/ClientApp/components/Dropdown.tsx create mode 100644 Clients/Xamarin.Interactive.Client.Web/ClientApp/components/RepresentationSelector.scss create mode 100644 Clients/Xamarin.Interactive.Client.Web/ClientApp/components/RepresentationSelector.tsx create mode 100644 Clients/Xamarin.Interactive.Client.Web/ClientApp/components/TransitionGroup.scss create mode 100644 Clients/Xamarin.Interactive.Client.Web/ClientApp/components/TransitionGroup.tsx rename Clients/Xamarin.Interactive.Client.Web/ClientApp/{components/CapturedOutputView.scss => renderers/CapturedOutputRenderer.scss} (100%) rename Clients/Xamarin.Interactive.Client.Web/ClientApp/{components/CapturedOutputView.tsx => renderers/CapturedOutputRenderer.tsx} (58%) delete mode 100644 Clients/Xamarin.Interactive.Client.Web/ClientApp/renderers/TestRenderer.tsx create mode 100644 Clients/Xamarin.Interactive.Client.Web/Icons.sketch diff --git a/Clients/Xamarin.Interactive.Client.Web/ClientApp/ResultRendererRegistry.ts b/Clients/Xamarin.Interactive.Client.Web/ClientApp/ResultRendererRegistry.ts deleted file mode 100644 index 9d784d2c8..000000000 --- a/Clients/Xamarin.Interactive.Client.Web/ClientApp/ResultRendererRegistry.ts +++ /dev/null @@ -1,57 +0,0 @@ -// -// Author: -// Aaron Bockover -// -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { CodeCellResult } from './evaluation' -import { ResultRendererFactory, ResultRenderer } from './rendering' - -import NullRenderer from './renderers/NullRenderer' -import CalendarRendererFactory from './renderers/CalendarRenderer' -import ToStringRendererFactory from './renderers/ToStringRenderer' -import ImageRendererFactory from './renderers/ImageRenderer' -import VerbatimHtmlRendererFactory from './renderers/VerbatimHtmlRenderer' -import TestRendererFactory from './renderers/TestRenderer' -import InteractiveObjectRendererFactory from './renderers/InteractiveObjectRenderer' -import TypeSpecRendererFactory from './renderers/TypeSystemRenderers'; - -export class ResultRendererRegistry { - private rendererFactories: ResultRendererFactory[] = [] - - register(factory: ResultRendererFactory) { - this.rendererFactories.push(factory) - } - - getRenderers(result: CodeCellResult): ResultRenderer[] { - if (result.isNullResult) - return [new NullRenderer]; - else if (result.resultRepresentations && result.resultRepresentations.length > 0) - return this.rendererFactories - .map(f => f(result)) - .filter(f => f !== null) - else - return [] - } - - static createDefault(): ResultRendererRegistry { - const registry = new ResultRendererRegistry - // More exciting and specific renderers should be first - registry.register(CalendarRendererFactory) - registry.register(ImageRendererFactory) - registry.register(VerbatimHtmlRendererFactory) - registry.register(TypeSpecRendererFactory) - - // These are 'catch all' and should always be last - // registry.register(InteractiveObjectRendererFactory) - registry.register(ToStringRendererFactory) - return registry - } - - static createForDesign(): ResultRendererRegistry { - const registry = ResultRendererRegistry.createDefault() - registry.register(TestRendererFactory) - return registry - } -} \ No newline at end of file diff --git a/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/ActionButton.scss b/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/ActionButton.scss new file mode 100644 index 000000000..f2e772976 --- /dev/null +++ b/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/ActionButton.scss @@ -0,0 +1,41 @@ +@import '../css/theme.scss'; + +button.ActionButton { + &.Small { + $hitBounds: 4px; + $visibleSize: 16px; + $size: 2 * $hitBounds + $visibleSize; + + width: $size; + height: $size; + padding: $hitBounds; + margin-bottom: -$hitBounds; + margin-right: -$hitBounds; + } + + cursor: pointer; + color: $themePrimary; + background: transparent; + border: none; + margin: 0; + outline: none; + + &:active { + outline: 1px dashed rgba($themePrimary, 0.4); + } + + svg { + fill: currentColor; + stroke: currentColor; + width: 100%; + height: 100%; + + @keyframes spin-cw { + 100% { transform: rotate(360deg); } + } + + @keyframes spin-ccw { + 100% { transform: rotate(-360deg); } + } + } +} \ No newline at end of file diff --git a/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/ActionButton.tsx b/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/ActionButton.tsx new file mode 100644 index 000000000..5504e5838 --- /dev/null +++ b/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/ActionButton.tsx @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as React from 'react' + +import './ActionButton.scss' + +export class ActionButton extends React.PureComponent<{ + iconName: string + title: string + onClick?: () => void +}> { + render() { + return ( + + ) + } + + private renderIcon() { + switch (this.props.iconName) { + case "CodeCell-Running": + return ( + + + + + + + + + + + ) + case "CodeCell-Run": + return ( + + + + ) + } + + return false; + } +} \ No newline at end of file diff --git a/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/CodeCell.tsx b/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/CodeCell.tsx index ff36722e8..940472f51 100644 --- a/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/CodeCell.tsx +++ b/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/CodeCell.tsx @@ -12,8 +12,7 @@ import { MonacoCellEditor, MonacoCellEditorProps } from './MonacoCellEditor' import { ContentBlock } from 'draft-js'; import { EditorMessage } from '../utils/EditorMessages' import { WorkbookShellContext } from './WorkbookShell' -import { ResultRendererRepresentation } from '../rendering'; -import { ResultRendererRegistry } from '../ResultRendererRegistry' +import { RepresentationRegistry } from '../rendering' import { MonacoCellMapper } from '../utils/MonacoUtils' import { @@ -37,7 +36,7 @@ import { interface CodeCellProps extends CodeCellViewProps { blockProps: { shellContext: WorkbookShellContext - rendererRegistry: ResultRendererRegistry + representationRegistry: RepresentationRegistry sendEditorMessage: (message: EditorMessage) => void cellMapper: MonacoCellMapper codeCellId: string @@ -100,8 +99,8 @@ export class CodeCell extends CodeCellView { } } - protected getRendererRegistry(): ResultRendererRegistry { - return this.props.blockProps.rendererRegistry + protected getRepresentationRegistry(): RepresentationRegistry { + return this.props.blockProps.representationRegistry } protected sendEditorMessage(message: EditorMessage) { diff --git a/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/CodeCellView.scss b/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/CodeCellView.scss index c95f8b978..7017cfff1 100644 --- a/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/CodeCellView.scss +++ b/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/CodeCellView.scss @@ -9,22 +9,56 @@ $margin: 8px; $padding: $margin / 2; +$borderColor: darken($cellEditorBackground, 20%); .CodeCell-container { - border: 1px solid $cellBorderColor; - padding: 0.5em; cursor: default; margin: 1em 0; - - .ms-Dropdown-container { - cursor: pointer; - } + border-left: 1px solid $borderColor; .CodeCell-editor-container { - margin-bottom: $margin; + position: relative; padding: $padding; - border: 1px solid $cellEditorBorderColor; background: $cellEditorBackground; + + .CodeCell-actions-container { + position: absolute; + z-index: 500; + bottom: 0; + right: 0; + + button { + position: absolute; + right: $padding; + bottom: $padding * 1.5; + + .CodeCell-Running-Cancel-Group { + visibility: hidden; + } + + &:hover { + .CodeCell-Running-Cancel-Group { + visibility: visible; + color: $themeCancel; + } + + .CodeCell-Running-Group { + visibility: hidden; + } + } + } + + svg .CodeCell-Running-Ring1, + svg .CodeCell-Running-Ring3 { + animation: spin-cw 1s linear infinite; + transform-origin: 50% 50%; + } + + svg .CodeCell-Running-Ring2 { + animation: spin-ccw 1s linear infinite; + transform-origin: 50% 50%; + } + } } .CodeCell-diagnostics-container { @@ -57,54 +91,26 @@ $padding: $margin / 2; } .CodeCell-captured-output-container { - border: 1px dotted $cellEditorBorderColor; - padding: $padding; + border-bottom: 1px dotted $cellEditorBorderColor; + background: lighten($cellEditorBackground, 2%); } - .CodeCell-results-container { + .CodeCell-results-container, + .CodeCell-captured-output-container { margin-bottom: $margin; - .CodeCell-result { + .CodeCell-representation { display: flex; - align-items: flex-end; - flex-wrap: wrap-reverse; - .CodeCell-result-renderer-container { - padding-right: 1em; + .RepresentationSelector { + margin-left: calc(-10vw - 1em); + width: 10vw; } - } - } - .CodeCell-actions-container { - .actions { - display: flex; - flex-direction: row; - align-items: stretch; - } - - .ms-Button, - .ms-Button-label, - .ms-Button-icon { - padding: 0; - margin: 0; - } - - .CancelButton:hover, - .CancelButton:hover * { - color: $cellCancelButtonTextColor; - } - - .ms-Button { - height: 22px; - } - - .ms-Button-label { - padding-left: 4px; - } - - .ms-ProgressIndicator { - padding-left: 8px; - flex-grow: 1; + .CodeCell-representation-renderer-container { + flex-grow: 1; + padding-left: calc(1em + #{$padding}); + } } } } \ No newline at end of file diff --git a/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/CodeCellView.tsx b/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/CodeCellView.tsx index 05095d114..ffe1bf696 100644 --- a/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/CodeCellView.tsx +++ b/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/CodeCellView.tsx @@ -5,41 +5,27 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import * as React from 'react'; -import * as ReactDOM from 'react-dom'; +import * as React from 'react' +import * as ReactDOM from 'react-dom' +import * as Immutable from 'immutable' -import { ActionButton } from 'office-ui-fabric-react/lib/Button'; import { ProgressIndicator } from 'office-ui-fabric-react/lib/ProgressIndicator'; import { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/Spinner'; -import { - Dropdown, - IDropdown, - DropdownMenuItemType, - IDropdownOption -} from 'office-ui-fabric-react/lib/Dropdown'; import { randomReactKey } from '../utils' +import { Dropdown } from './Dropdown' +import { ActionButton } from './ActionButton' import { EditorMessage, EditorMessageType } from '../utils/EditorMessages' import { CodeCellResult, CodeCellResultHandling, Diagnostic, CapturedOutputSegment } from '../evaluation' -import { ResultRendererRepresentation } from '../rendering' -import { ResultRendererRegistry } from '../ResultRendererRegistry' -import { CapturedOutputView } from './CapturedOutputView' +import { Representation, RepresentationRegistry } from '../rendering' +import createCapturedOutputRepresentation from '../renderers/CapturedOutputRenderer' +import { RepresentationSelector } from './RepresentationSelector' import './CodeCellView.scss' -export interface ResultRendererRepresentationMap { - [key: string]: ResultRendererRepresentation -} - -export interface CodeCellResultRendererState { - result: CodeCellResult - representations: ResultRendererRepresentationMap - selectedRepresentationKey: string -} - export const enum CodeCellViewStatus { Unbound, Ready, @@ -47,14 +33,46 @@ export const enum CodeCellViewStatus { Aborting } +interface RepresentationViewProps { + rootRepresentation: Representation +} + +interface RepresentationViewState { + selectedRepresentation?: Representation +} + +class RepresentationView extends React.PureComponent { + constructor(props: RepresentationViewProps) { + super(props) + this.state = {} + } + + render() { + return ( +
+ this.setState({ selectedRepresentation })}/> +
+ {this.state.selectedRepresentation && this.state.selectedRepresentation.component && + } +
+
+ ) + } +} + export interface CodeCellViewProps { - rendererRegistry: ResultRendererRegistry + representationRegistry: RepresentationRegistry } export interface CodeCellViewState { status: CodeCellViewStatus capturedOutput: CapturedOutputSegment[] - results: CodeCellResultRendererState[] + results: JSX.Element[] diagnostics: Diagnostic[] } @@ -63,76 +81,48 @@ export abstract class CodeCellView< TCodeCellViewState extends CodeCellViewState = CodeCellViewState> extends React.Component { - protected abstract getRendererRegistry(): ResultRendererRegistry - protected abstract abortEvaluation(): Promise + protected abstract getRepresentationRegistry(): RepresentationRegistry protected abstract startEvaluation(): Promise + protected abstract abortEvaluation(): Promise protected abstract renderEditor(): any protected setStateFromResult(result: CodeCellResult, resultHandling?: CodeCellResultHandling) { - const reps = this - .getRendererRegistry() - .getRenderers(result) - .map(r => r.getRepresentations(result)) - - const flatReps = reps.length === 0 - ? [] - : reps.reduce((a, b) => a.concat(b)) + const rootRepresentation = this + .getRepresentationRegistry() + .getRepresentations(result) - const mapReps: ResultRendererRepresentationMap = {} - flatReps.map(rep => mapReps[rep.key] = rep) + const resultView = rootRepresentation && - const rendererState = { - result: result, - representations: mapReps, - selectedRepresentationKey: flatReps[0].key - } - - if (!resultHandling) - resultHandling = result.resultHandling - - switch (resultHandling) { + switch (resultHandling || result.resultHandling) { case CodeCellResultHandling.Append: - this.setState({ - results: this.state.results.concat(rendererState) - }) + if (resultView) + this.setState({ results: this.state.results.concat(resultView) }) break case CodeCellResultHandling.Replace: default: - this.setState({ - results: [rendererState] - }) + this.setState({ results: resultView ? [resultView] : [] }) break } } private renderActions() { switch (this.state.status) { - case CodeCellViewStatus.Unbound: - return null case CodeCellViewStatus.Evaluating: - return ( -
- this.abortEvaluation()}> - Cancel - - -
- ) case CodeCellViewStatus.Aborting: - return
Aborting...
+ return this.abortEvaluation()}/> case CodeCellViewStatus.Ready: - return ( - this.startEvaluation()}> - Run - - ) + return this.startEvaluation()}/> } - return
+ + return false } render() { @@ -140,6 +130,9 @@ export abstract class CodeCellView<
{this.renderEditor()} +
+ {this.renderActions()} +
    @@ -168,50 +161,16 @@ export abstract class CodeCellView<
{this.state.capturedOutput.length > 0 &&
- +
}
- {this.state.results.map((resultState, i) => { - const representationKeys = Object.keys(resultState.representations) - if (representationKeys.length === 0) - return - const dropdownOptions = representationKeys.length > 1 - ? representationKeys.map(key => { - return { - key: key, - text: resultState.representations[key].displayName - } - }) - : null - - let repElem = null - if (resultState.selectedRepresentationKey) { - const rep = resultState.representations[resultState.selectedRepresentationKey] - repElem = - } - - return ( -
-
- {repElem} -
- {dropdownOptions && { - resultState.selectedRepresentationKey = item.key as string - this.setState(this.state) - }}/>} -
- ) - })} -
-
- {this.renderActions()} + {this.state.results}
); @@ -258,8 +217,8 @@ export class MockedCodeCellView extends CodeCellView { this.setStateFromPendingResult() } - protected getRendererRegistry(): ResultRendererRegistry { - return this.props.rendererRegistry + protected getRepresentationRegistry(): RepresentationRegistry { + return this.props.representationRegistry } protected async startEvaluation(): Promise { diff --git a/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/ComponentPlayground.tsx b/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/ComponentPlayground.tsx index 3fd9599be..3f826d324 100644 --- a/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/ComponentPlayground.tsx +++ b/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/ComponentPlayground.tsx @@ -8,14 +8,16 @@ import * as React from 'react'; import { RouteComponentProps } from 'react-router'; -import { CodeCellResult, CodeCellResultHandling, CodeCellEventType } from '../evaluation' -import { ResultRendererRegistry } from '../ResultRendererRegistry' +import { CodeCellResult, CodeCellEvaluationStatus, CodeCellResultHandling, CodeCellEventType } from '../evaluation' +import { createDefaultRegistry, createDesignRegistry } from '../rendering' import { MockedCodeCellView } from './CodeCellView'; +import { TestRepresentationSelector } from './RepresentationSelector'; export class ComponentPlayground extends React.Component> { private readonly nullResult: CodeCellResult = { $type: CodeCellEventType.Evaluation, codeCellId: '85cd037b-4cb6-4489-a854-912959b60a6b/3fb9e8a3-2c29-429d-b417-e2678761b57e', + status: CodeCellEvaluationStatus.Success, isNullResult: false, evaluationDuration: '', cultureLCID: 1033, @@ -28,6 +30,7 @@ export class ComponentPlayground extends React.Component private readonly numberResult: CodeCellResult = { $type: CodeCellEventType.Evaluation, codeCellId: '65a6fd4c-696f-4b2f-9d0d-3bf452e69f5f/eab2b254-9047-4670-bdb1-5e24aefa4843', + status: CodeCellEvaluationStatus.Success, isNullResult: false, evaluationDuration: '', cultureLCID: 1033, @@ -69,6 +72,7 @@ export class ComponentPlayground extends React.Component private readonly dateTimeResult: CodeCellResult = { $type: CodeCellEventType.Evaluation, codeCellId: 'dca76582-6c22-4c64-9893-2270a67552ce/e9234d19-89a4-4e43-a7cd-780f3fd04541', + status: CodeCellEvaluationStatus.Success, isNullResult: false, evaluationDuration: '', cultureLCID: 1033, @@ -84,29 +88,80 @@ export class ComponentPlayground extends React.Component ] } + private readonly typeResult: CodeCellResult = { + $type: CodeCellEventType.Evaluation, + codeCellId: '12cc4902-1783-4f74-850e-e63474d6b2af/0eba8723-2689-45c2-970f-c5200c8e582f', + status: CodeCellEvaluationStatus.Success, + resultHandling: CodeCellResultHandling.Replace, + resultType: 'System.RuntimeType', + isNullResult: false, + resultRepresentations: [ + { + $type: 'Xamarin.Interactive.Representations.Reflection.TypeNode', + typeName: { + $type: 'Xamarin.Interactive.Representations.Reflection.TypeSpec', + name: { + $type: 'Xamarin.Interactive.Representations.Reflection.TypeSpec+TypeName', + namespace: 'System', + name: 'Int32' + }, + assemblyName: 'System.Private.CoreLib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e' + } + }, + { + $type: 'Xamarin.Interactive.Representations.ToStringRepresentation', + formats: [ + { + $type: 'Xamarin.Interactive.Representations.ToStringRepresentation+Format', + name: '{0}', + value: 'System.Int32' + } + ] + }, + { + $type: 'Xamarin.Interactive.Representations.ReflectionInteractiveObject', + hasMembers: true, + toStringRepresentation: 'System.Int32', + handle: 2, + representedObjectHandle: 1, + representedType: 'System.RuntimeType' + } + ], + evaluationDuration: '00:00:00.0040860', + cultureLCID: 1033, + uiCultureLCID: 1033 + } + public render() { return (
-

Code Cell

+ {/*

Code Cell

Code Cell with Number Result

Code Cell with DateTime Result

+

Code Cell with Type Result

+ +

Code Cell with Multiple Results

+ resultHandling={CodeCellResultHandling.Append}/> */} + +
) } diff --git a/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/Dropdown.scss b/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/Dropdown.scss new file mode 100644 index 000000000..ecf47818e --- /dev/null +++ b/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/Dropdown.scss @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +.Dropdown { + display: inline-flex; + flex-direction: column; + + * { + margin: 0; + padding: 0; + outline: none; + font-size: 1em; + color: currentColor; + cursor: default; + } + + button { + border: none; + text-align: inherit; + + .DropdownButtonExpander { + svg { + stroke: currentColor; + fill: currentColor; + } + } + } + + button, + li { + display: flex; + + div:first-child { + flex-grow: 1; + } + + div:last-child { + flex-shrink: 1; + padding-left: 0.05rem; + width: 1em; + height: 1em; + } + } + + ul { + list-style: none; + } +} \ No newline at end of file diff --git a/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/Dropdown.tsx b/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/Dropdown.tsx new file mode 100644 index 000000000..0fb94d2c3 --- /dev/null +++ b/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/Dropdown.tsx @@ -0,0 +1,142 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as React from 'react' +import * as ReactDOM from 'react-dom' +import { randomReactKey, classNames } from '../utils'; + +import { TransitionGroup } from './TransitionGroup'; + +import './Dropdown.scss' + +export interface DropdownOption { + key: React.Key + text: string +} + +export interface DropdownProps { + className?: string + options: DropdownOption[] + defaultSelectedKey?: React.Key + onChanged?: (selectedKey: React.Key) => void +} + +interface DropdownState { + selectedKey: React.Key + menuState: 'hidden' | 'visible' +} + +export class Dropdown extends React.PureComponent { + private buttonRef?: HTMLButtonElement + private menuRef?: HTMLUListElement + + constructor(props: DropdownProps) { + super(props) + + this.state = { + selectedKey: props.defaultSelectedKey || props.options [0].key, + menuState: 'hidden' + } + + this.onDocumentMouseDown = this.onDocumentMouseDown.bind(this) + } + + private onDocumentMouseDown(e: MouseEvent) { + let hitTest = false + + for (const elem of [this.menuRef, this.buttonRef]) { + if (elem) { + const rect = elem.getBoundingClientRect() + if (e.pageX >= rect.left && + e.pageX <= rect.right && + e.pageY >= rect.top && + e.pageY <= rect.bottom) { + hitTest = true + break + } + } + } + + if (!hitTest) + this.hideMenu() + } + + private toggleMenu() { + if (this.state.menuState === 'visible') + this.hideMenu() + else + this.showMenu() + } + + private showMenu() { + this.setState({ menuState: 'visible' }) + document.addEventListener('mousedown', this.onDocumentMouseDown) + } + + private hideMenu() { + this.setState({ menuState: 'hidden' }) + document.removeEventListener('mousedown', this.onDocumentMouseDown) + } + + private selectItem(key: React.Key) { + this.setState({ selectedKey: key }) + this.hideMenu() + + if (this.props.onChanged) + this.props.onChanged(key) + } + + render() { + const { menuState, selectedKey } = this.state + + const selectedOption = this.props.options.find(o => o.key === selectedKey) + if (!selectedOption) + return false + + const menuId = randomReactKey() + + return ( +
+ + +
    this.menuRef = node as HTMLMenuElement}> + {this.props.options.map((option, i) => { + const isSelected = option.key === selectedOption.key + return
  • this.selectItem(option.key)}> +
    {option.text}
    +
    +
  • + })} +
+
+
+ ) + } +} \ No newline at end of file diff --git a/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/RepresentationSelector.scss b/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/RepresentationSelector.scss new file mode 100644 index 000000000..3761a0af1 --- /dev/null +++ b/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/RepresentationSelector.scss @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +@import '../css/theme.scss'; + +.RepresentationSelector { + + .Dropdown { + display: flex; + color: $representationSelectorColor; + + text-align: right; + + * { + cursor: pointer; + user-select: none; + @include codeFont; + } + + .DropdownButton { + border-bottom: 1px solid transparent; + } + + &.DropdownActive .DropdownButton, + .DropdownButton:hover, + li:hover { + color: $representationSelectorActiveColor; + } + + .DropdownButtonExpander svg { + color: $representationSelectorColor; + transform: rotate(-90deg); + transition: transform 0.25s ease-in-out; + } + + &.DropdownActive .DropdownButton .DropdownButtonExpander svg { + transform: rotate(0deg); + } + } +} \ No newline at end of file diff --git a/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/RepresentationSelector.tsx b/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/RepresentationSelector.tsx new file mode 100644 index 000000000..811083598 --- /dev/null +++ b/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/RepresentationSelector.tsx @@ -0,0 +1,180 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as React from 'react' +import { randomReactKey, classNames } from '../utils' +import { Representation } from '../rendering' +import { Dropdown } from './Dropdown'; +import { TransitionGroup, transitionHeight } from './TransitionGroup' + +import './RepresentationSelector.scss' +import { List } from 'immutable'; + +function findPathToRepresentation( + representation: Representation | null, + predicate: (representation: Representation) => boolean): Representation[] | null { + if (!representation) + return null + + if (predicate(representation)) + return [representation] + + if (!representation.children) + return null + + for (let i = 0; i < representation.children.size; i++) { + const path = findPathToRepresentation( + representation.children.get(i), + predicate) + if (path) { + path.unshift(representation) + return path + } + } + + return null +} + +function findPathToRepresentationByKey( + representation: Representation, + key: React.Key): Representation[] | null { + return findPathToRepresentation( + representation, + representation => representation.key === key) +} + +interface RepresentationSelectorProps { + rootRepresentation: Representation + onRenderRepresentation?: (representation: Representation) => void +} + +interface RepresentationSelectorState { + selectedPath: Representation[] | null +} + +export class RepresentationSelector extends React.PureComponent< + RepresentationSelectorProps, RepresentationSelectorState> { + constructor(props: RepresentationSelectorProps) { + super(props) + + this.onSelectedKeyChanged = this.onSelectedKeyChanged.bind(this) + + this.state = { + selectedPath: this.selectKey(null) + } + + setTimeout(() => this.raiseOnRepresentationSelected(), 0) + } + + private selectKey(selectedKey: React.Key | null): Representation[] | null { + // try to find a representation by key if we have one, + // falling back to the first one that is renderable if + // not or the key is not found + return (selectedKey && + findPathToRepresentationByKey( + this.props.rootRepresentation, + selectedKey)) || + findPathToRepresentation( + this.props.rootRepresentation, + representation => !!representation.component) + } + + private raiseOnRepresentationSelected() { + const { selectedPath } = this.state + if (selectedPath && selectedPath.length > 0 && this.props.onRenderRepresentation) + this.props.onRenderRepresentation(selectedPath[selectedPath.length - 1]) + } + + private onSelectedKeyChanged(selectedKey: React.Key) { + this.setState({ selectedPath: this.selectKey(selectedKey) }) + } + + componentDidUpdate() { + this.raiseOnRepresentationSelected() + } + + private renderDropdowns( + dropdowns: JSX.Element[], + selectedPath: Representation[], + representation: Representation) { + if (!representation || !representation.children || representation.children.size === 0) + return + + const options = representation.children.map(representation => { + return { + key: representation!.key, + text: representation!.displayName + } + }).toArray() + + const inSelectionPath = selectedPath[0].key === representation.key + + dropdowns.push( transitionHeight(elem, inSelectionPath)} + key={dropdowns.length} + options={options} + defaultSelectedKey={inSelectionPath && selectedPath.length > 1 ? selectedPath[1].key : undefined} + onChanged={this.onSelectedKeyChanged}/>) + + representation.children.forEach(child => { + this.renderDropdowns( + dropdowns, + selectedPath.slice(1), + child!) + }) + } + + render() { + if (!this.state.selectedPath) + return false + + const dropdowns: JSX.Element[] = [] + this.renderDropdowns( + dropdowns, + this.state.selectedPath, + this.props.rootRepresentation) + + return ( +
+ {dropdowns} +
+ ) + } +} + +function randomInteger(min: number, max: number) { + return Math.floor(Math.random() * (max - min + 1)) + min +} + +export class TestRepresentationSelector extends React.PureComponent { + private rootRepresentation: Representation = this.createRepresentation('', 0)! + + private createRepresentation(displayName: string, depth: number): Representation | null { + if (depth > 4) + return null + + const representation: Representation = { + key: randomReactKey(), + displayName: displayName, + children: List( + new Array(randomInteger(depth < 1 ? 1 : 0, 5)) + .fill(undefined) + .map((_, i) => this.createRepresentation( + `${displayName && `${displayName}.`}${i}`, + depth + 1)) + .filter(representation => representation)) + } + + if (!representation.children || representation.children.size === 0) + (representation as any).component =
Hi
+ + return representation + } + + render() { + return ( + + ) + } +} \ No newline at end of file diff --git a/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/TransitionGroup.scss b/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/TransitionGroup.scss new file mode 100644 index 000000000..97ffa5ad9 --- /dev/null +++ b/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/TransitionGroup.scss @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +$transitionDuration: 150ms; + +.TransitionGroup { + transition: opacity $transitionDuration ease-out, max-height $transitionDuration ease-out; + overflow: hidden; + + opacity: 1; + + &.TransitionGroupHidden { + transition: max-height $transitionDuration ease-in, opacity $transitionDuration ease-in; + opacity: 0; + max-height: 0 !important; + } +} \ No newline at end of file diff --git a/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/TransitionGroup.tsx b/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/TransitionGroup.tsx new file mode 100644 index 000000000..2aaefd85b --- /dev/null +++ b/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/TransitionGroup.tsx @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as React from 'react' +import * as ReactDOM from 'react-dom' +import { classNames } from '../utils'; + +import './TransitionGroup.scss' + +export interface TransitionProps { + visible: boolean +} + +export class TransitionGroup extends React.PureComponent { + constructor(props: TransitionProps) { + super(props) + this.state = {} + } + render() { + return ( +
transitionHeight(elem, this.props.visible, true)}> + {this.props.children} +
+ ) + } +} + +export function transitionHeight( + elem: HTMLElement | React.ReactInstance | null, + visible: boolean, + applyMaxHeight?: boolean) { + if (!elem) + return undefined + + if (!(elem instanceof HTMLElement)) + elem = ReactDOM.findDOMNode(elem) as HTMLElement + + if (!(elem instanceof HTMLElement)) + return undefined + + if (applyMaxHeight) + elem.style.maxHeight = elem.scrollHeight + 'px' + + elem.classList.add('TransitionGroup') + + if (visible) + elem.classList.remove('TransitionGroupHidden') + else + elem.classList.add('TransitionGroupHidden') +} \ No newline at end of file diff --git a/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/WorkbookEditor.tsx b/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/WorkbookEditor.tsx index 8e4952149..0b59efe16 100644 --- a/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/WorkbookEditor.tsx +++ b/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/WorkbookEditor.tsx @@ -142,7 +142,7 @@ export class WorkbookEditor extends React.Component this.sendMessage(message), cellMapper: this, codeCellId, diff --git a/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/WorkbookShell.tsx b/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/WorkbookShell.tsx index 8a0a688d9..d7b8095de 100644 --- a/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/WorkbookShell.tsx +++ b/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/WorkbookShell.tsx @@ -15,7 +15,7 @@ import { osMac } from '../utils' import { WorkbookSession, SessionEvent, SessionEventKind, SdkId } from '../WorkbookSession' import { WorkbookCommandBar } from './WorkbookCommandBar' import { WorkbookEditor } from './WorkbookEditor' -import { ResultRendererRegistry } from '../ResultRendererRegistry' +import { RepresentationRegistry, createDefaultRegistry } from '../rendering' import { PackageSearch } from './PackageSearch' import { StatusMessageBar } from './StatusMessageBar' import { StatusUIAction, MessageKind, MessageSeverity } from '../messages' @@ -25,7 +25,7 @@ import { loadWorkbookFromString, loadWorkbookFromWorkbookPackage, loadWorkbookFr export interface WorkbookShellContext { session: WorkbookSession - rendererRegistry: ResultRendererRegistry + representationRegistry: RepresentationRegistry } interface WorkbookShellState { @@ -56,7 +56,7 @@ export class WorkbookShell extends React.Component { this.shellContext = { session: new WorkbookSession, - rendererRegistry: ResultRendererRegistry.createDefault() + representationRegistry: createDefaultRegistry() } this.state = { diff --git a/Clients/Xamarin.Interactive.Client.Web/ClientApp/css/theme.scss b/Clients/Xamarin.Interactive.Client.Web/ClientApp/css/theme.scss index fcf803d0a..9a4808dcd 100644 --- a/Clients/Xamarin.Interactive.Client.Web/ClientApp/css/theme.scss +++ b/Clients/Xamarin.Interactive.Client.Web/ClientApp/css/theme.scss @@ -4,9 +4,9 @@ $fgColorError: #f56d4f; $fgColorWarning: #f38f00; -$codeFontFamily: Menlo, Monaco, "Courier New", monospace; -$codeFontSize: 12px; -$codeLineHeight: $codeFontSize + 2 * 2px; +$codeFontFamily: Menlo, Monaco, Consolas, "Courier New", monospace; +$codeFontSize: 14px; +$codeLineHeight: $codeFontSize * 1.5; @mixin codeFont() { font-family: $codeFontFamily; @@ -19,9 +19,11 @@ $codeLineHeight: $codeFontSize + 2 * 2px; max-height: calc(#{$codeLineHeight} * #{$maxLineCount} + (#{$codeLineHeight} - #{$codeFontSize}) / 2); } -$cellBorderColor: #ccc; - $cellEditorBorderColor: #ddd; -$cellEditorBackground: #fafafa; +$cellEditorBackground: #f7f7f7; + +$representationSelectorColor: #777; +$representationSelectorActiveColor: #222; -$cellCancelButtonTextColor: #c00; \ No newline at end of file +$themePrimary: #5c2d91; +$themeCancel: #a80000; \ No newline at end of file diff --git a/Clients/Xamarin.Interactive.Client.Web/ClientApp/evaluation.ts b/Clients/Xamarin.Interactive.Client.Web/ClientApp/evaluation.ts index f8b9b3849..f4843b15b 100644 --- a/Clients/Xamarin.Interactive.Client.Web/ClientApp/evaluation.ts +++ b/Clients/Xamarin.Interactive.Client.Web/ClientApp/evaluation.ts @@ -68,6 +68,7 @@ export interface CodeCellUpdate extends ICodeCellEvent { } export interface CodeCellResult extends ICodeCellEvent { + status: CodeCellEvaluationStatus resultHandling: CodeCellResultHandling resultType: string | null resultRepresentations: any[] diff --git a/Clients/Xamarin.Interactive.Client.Web/ClientApp/renderers/CalendarRenderer.tsx b/Clients/Xamarin.Interactive.Client.Web/ClientApp/renderers/CalendarRenderer.tsx index abbda2427..3cfd82ec4 100644 --- a/Clients/Xamarin.Interactive.Client.Web/ClientApp/renderers/CalendarRenderer.tsx +++ b/Clients/Xamarin.Interactive.Client.Web/ClientApp/renderers/CalendarRenderer.tsx @@ -10,28 +10,18 @@ import { Calendar, DayOfWeek } from 'office-ui-fabric-react/lib/Calendar' -import { randomReactKey } from '../utils' import { CodeCellResult } from '../evaluation' -import { ResultRenderer, ResultRendererRepresentation } from '../rendering' +import { createComponentRepresentation } from '../rendering' -export default function CalendarRendererFactory(result: CodeCellResult) { - return result.resultType === 'System.DateTime' && - typeof result.resultRepresentations[0] === 'string' - ? new CalendarRenderer - : null -} - -class CalendarRenderer implements ResultRenderer { - getRepresentations(result: CodeCellResult) { - return [{ - key: randomReactKey(), - displayName: 'Calendar', - component: CalendarRepresentation, - componentProps: { +export default function createCalenderRepresentation(result: CodeCellResult) { + if (result.resultType === 'System.DateTime' && typeof result.resultRepresentations[0] === 'string') + return createComponentRepresentation( + 'Calendar', + CalendarRenderer, + { value: new Date(result.resultRepresentations[0]) - } - }] - } + }) + return null } const DayPickerStrings = { @@ -88,7 +78,7 @@ const DayPickerStrings = { goToToday: 'Go to today' } -class CalendarRepresentation extends React.Component<{ value: Date }> { +class CalendarRenderer extends React.Component<{ value: Date }> { render() { return { +export default function createCapturedOutputRepresentation(segments: CapturedOutputSegment[]) { + return createComponentRepresentation( + 'Console', + CapturedOutputRenderer, + { segments }) +} + +class CapturedOutputRenderer extends React.Component<{ segments: CapturedOutputSegment[] }> { render() { return (
{ - if (span) - span.scrollIntoView({ behavior: 'smooth' }) + // if (span) + // span.scrollIntoView({ behavior: 'smooth' }) }}> {segment.value} diff --git a/Clients/Xamarin.Interactive.Client.Web/ClientApp/renderers/ImageRenderer.tsx b/Clients/Xamarin.Interactive.Client.Web/ClientApp/renderers/ImageRenderer.tsx index f848fd4b7..943d61132 100644 --- a/Clients/Xamarin.Interactive.Client.Web/ClientApp/renderers/ImageRenderer.tsx +++ b/Clients/Xamarin.Interactive.Client.Web/ClientApp/renderers/ImageRenderer.tsx @@ -1,35 +1,18 @@ -// -// Author: -// Larry Ewing -// // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. import * as React from 'react' import { CodeCellResult } from '../evaluation' import { - ResultRenderer, - ResultRendererRepresentation, getRepresentationsOfType, - getFirstRepresentationOfType + createContainerRepresentation, + createComponentRepresentation } from '../rendering' -import { randomReactKey } from '../utils' -import { - Image, - IImageProps, - ImageFit -} from 'office-ui-fabric-react/lib/Image' import './ImageRenderer.scss' const RepresentationName = 'Xamarin.Interactive.Representations.Image' -export default function ImageRendererFactory(result: CodeCellResult) { - return getFirstRepresentationOfType(result, RepresentationName) - ? new ImageRenderer - : null -} - interface ImageData { $type: string format: string @@ -42,43 +25,24 @@ interface ImageData { scale: number } -class ImageRenderer implements ResultRenderer { - getRepresentations(result: CodeCellResult) { - const reps = [] - - for (const image of getRepresentationsOfType(result, RepresentationName)) { - if (!image.format || !image.data) - continue; - - reps.push({ - key: randomReactKey(), - displayName: 'Image', - component: ImageRepresentation, - componentProps: { - value: 'Image', - image: image - } - }) - } - - return reps - } +export default function createImageRepresentation(result: CodeCellResult) { + return createContainerRepresentation( + 'Image', + getRepresentationsOfType(result, RepresentationName) + .filter(image => image && image.format && image.data) + .map(image => createComponentRepresentation( + image.format, + ImageRepresentation, + image))) } -class ImageRepresentation extends React.Component<{ value: string, image: ImageData }> { +class ImageRepresentation extends React.Component { render() { - const image = this.props.image; + const image = this.props; const size = image.width > 0 ? { width: image.width, height: image.height} : {} - const imageProps: IImageProps = { - imageFit: ImageFit.contain, - maximizeFrame: true - } const src = image.format === 'Uri' ? atob(image.data.$value) : `data:${image.format};base64,${image.data.$value}` - - // FIXME: Fabric's is causing some strange clipping with at least SVG - // return - return + return } } \ No newline at end of file diff --git a/Clients/Xamarin.Interactive.Client.Web/ClientApp/renderers/InteractiveObjectRenderer.tsx b/Clients/Xamarin.Interactive.Client.Web/ClientApp/renderers/InteractiveObjectRenderer.tsx index 10f3baee5..c518844a8 100644 --- a/Clients/Xamarin.Interactive.Client.Web/ClientApp/renderers/InteractiveObjectRenderer.tsx +++ b/Clients/Xamarin.Interactive.Client.Web/ClientApp/renderers/InteractiveObjectRenderer.tsx @@ -5,6 +5,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +/* + import * as React from 'react' import { randomReactKey } from '../utils' import { CodeCellResult } from '../evaluation'; @@ -54,3 +56,5 @@ class InteractiveObjectRepresentation extends React.Component<{ handle: string } return {this.props.handle} } } + +*/ \ No newline at end of file diff --git a/Clients/Xamarin.Interactive.Client.Web/ClientApp/renderers/NullRenderer.tsx b/Clients/Xamarin.Interactive.Client.Web/ClientApp/renderers/NullRenderer.tsx index 2f52e1869..dff09f5fd 100644 --- a/Clients/Xamarin.Interactive.Client.Web/ClientApp/renderers/NullRenderer.tsx +++ b/Clients/Xamarin.Interactive.Client.Web/ClientApp/renderers/NullRenderer.tsx @@ -1,27 +1,15 @@ -// -// Author: -// Aaron Bockover -// // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. import * as React from 'react' -import { randomReactKey } from '../utils' -import { CodeCellResult } from '../evaluation' -import { ResultRenderer } from '../rendering' +import { createComponentRepresentation } from '../rendering' -export default class NullRenderer implements ResultRenderer { - getRepresentations(result: CodeCellResult) { - return [{ - key: randomReactKey(), - component: NullRepresentation, - displayName: 'null' - }] - } +export default function createNullRepresentation() { + return createComponentRepresentation('null', NullRenderer) } -class NullRepresentation extends React.Component { +class NullRenderer extends React.Component { render() { - return
null
+ return null } } \ No newline at end of file diff --git a/Clients/Xamarin.Interactive.Client.Web/ClientApp/renderers/TestRenderer.tsx b/Clients/Xamarin.Interactive.Client.Web/ClientApp/renderers/TestRenderer.tsx deleted file mode 100644 index 49e76fee3..000000000 --- a/Clients/Xamarin.Interactive.Client.Web/ClientApp/renderers/TestRenderer.tsx +++ /dev/null @@ -1,46 +0,0 @@ -// -// Author: -// Aaron Bockover -// -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import * as React from 'react' -import { randomReactKey } from '../utils' -import { CodeCellResult } from '../evaluation'; -import { ResultRenderer } from '../rendering' - -export default function TestRendererFactory(result: CodeCellResult) { - if (!result.resultRepresentations || result.resultRepresentations.length === 0) - return new TestRenderer - return null -} - -class TestRenderer implements ResultRenderer { - getRepresentations(result: CodeCellResult) { - return [ - { - key: randomReactKey(), - component: TestRepresentation, - componentProps: { - id: 1 - }, - displayName: 'Test Representation 1' - }, - { - key: randomReactKey(), - component: TestRepresentation, - componentProps: { - id: 2 - }, - displayName: 'Test Representation 2' - } - ] - } -} - -class TestRepresentation extends React.Component<{ id: number }> { - render() { - return
Test Rendering: {this.props.id}
- } -} \ No newline at end of file diff --git a/Clients/Xamarin.Interactive.Client.Web/ClientApp/renderers/ToStringRenderer.tsx b/Clients/Xamarin.Interactive.Client.Web/ClientApp/renderers/ToStringRenderer.tsx index df77c4971..ef52ccb8e 100644 --- a/Clients/Xamarin.Interactive.Client.Web/ClientApp/renderers/ToStringRenderer.tsx +++ b/Clients/Xamarin.Interactive.Client.Web/ClientApp/renderers/ToStringRenderer.tsx @@ -1,17 +1,13 @@ -// -// Author: -// Aaron Bockover -// // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. import * as React from 'react' -import { randomReactKey } from '../utils' import { CodeCellResult } from '../evaluation' import { - ResultRenderer, - ResultRendererRepresentation, - getFirstRepresentationOfType + Representation, + getFirstRepresentationOfType, + createContainerRepresentation, + createComponentRepresentation } from '../rendering' export const ToStringRepresentationDataTypeName = 'Xamarin.Interactive.Representations.ToStringRepresentation' @@ -24,41 +20,27 @@ export interface ToStringRepresentationData { }[] } -export default function ToStringRendererFactory(result: CodeCellResult) { - return getFirstRepresentationOfType(result, ToStringRepresentationDataTypeName) - ? new ToStringRenderer - : null -} +export default function createToStringRepresentation(result: CodeCellResult): Representation | null { + const value = getFirstRepresentationOfType( + result, + ToStringRepresentationDataTypeName) -class ToStringRenderer implements ResultRenderer { - getRepresentations(result: CodeCellResult) { - const reps: ResultRendererRepresentation[] = [] - const value = getFirstRepresentationOfType( - result, - ToStringRepresentationDataTypeName) + if (!value) + return null - if (value) { - // TODO: some way to toggle between current culture (what we're using now, - // index 0), and invariant culture (completely ignoring it, index 1). We - // have never exposed the invariant culture, so this is not a regression - // over the XCB UI. - for (const format of value.formats) { - reps.push({ - key: randomReactKey(), - displayName: format.name, - component: ToStringRepresentation, - componentProps: { - value: format.value - } - }) - } - } + const rr = createContainerRepresentation( + 'ToString', + value.formats.map(format => createComponentRepresentation( + format.name, + ToStringRenderer, + { value: format.value }))) - return reps - } + console.log("CREATED REP: %O", rr) + + return rr } -class ToStringRepresentation extends React.Component<{ value: string }> { +class ToStringRenderer extends React.Component<{ value: string }> { render() { return {this.props.value} } diff --git a/Clients/Xamarin.Interactive.Client.Web/ClientApp/renderers/TypeSystemRenderers.tsx b/Clients/Xamarin.Interactive.Client.Web/ClientApp/renderers/TypeSystemRenderers.tsx index 4aab396c5..d61a329a0 100644 --- a/Clients/Xamarin.Interactive.Client.Web/ClientApp/renderers/TypeSystemRenderers.tsx +++ b/Clients/Xamarin.Interactive.Client.Web/ClientApp/renderers/TypeSystemRenderers.tsx @@ -1,16 +1,11 @@ -// -// Author: -// Aaron Bockover -// // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. import * as React from 'react' import { CodeCellResult } from '../evaluation'; import { - ResultRenderer, - ResultRendererRepresentation, - getFirstRepresentationOfType + getFirstRepresentationOfType, + createComponentRepresentation } from '../rendering' import { randomReactKey } from '../utils' @@ -42,38 +37,28 @@ interface TypeSpec { typeArguments?: TypeSpec[] } -export default function TypeSpecRendererFactory(result: CodeCellResult) { - return getFirstRepresentationOfType(result, TypeNodeDataTypeName) - ? new TypeSpecRenderer - : null -} - -class TypeSpecRenderer implements ResultRenderer { - getRepresentations(result: CodeCellResult) { - const value = getFirstRepresentationOfType( - result, - TypeNodeDataTypeName) +export default function createTypeRepresentation(result: CodeCellResult) { + const value = getFirstRepresentationOfType( + result, + TypeNodeDataTypeName) - if (value) - return [{ - key: randomReactKey(), - displayName: 'Type', - component: TypeRepresentation, - componentProps: { value } - }] + if (!value) + return null - return [] - } + return createComponentRepresentation( + 'Type', + TypeRenderer, + { value }) } -interface TypeNameRepresentationProps { +interface TypeNameRendererProps { languageName: LanguageName typeName: TypeName typeArguments?: TypeSpec[] isNestedName?: boolean } -class TypeNameRepresentation extends React.PureComponent { +class TypeNameRenderer extends React.PureComponent { render(): JSX.Element { const typeArguments = (this.props.typeArguments && this.props.typeArguments.length > 0 ? this.props.typeArguments @@ -82,7 +67,7 @@ class TypeNameRepresentation extends React.PureComponent { i > 0 && ', '} - { typeArgument && } @@ -141,12 +126,12 @@ class TypeNameRepresentation extends React.PureComponent { +class TypeSpecRenderer extends React.PureComponent { render() { const ts = this.props.typeSpec let typeArgumentOffset = 0 @@ -160,7 +145,7 @@ class TypeSpecRepresentation extends React.PureComponent { +class TypeRenderer extends React.Component<{ value: Type }> { render() { return ( - diff --git a/Clients/Xamarin.Interactive.Client.Web/ClientApp/renderers/VerbatimHtmlRenderer.tsx b/Clients/Xamarin.Interactive.Client.Web/ClientApp/renderers/VerbatimHtmlRenderer.tsx index 0e497a027..4abbca1cc 100644 --- a/Clients/Xamarin.Interactive.Client.Web/ClientApp/renderers/VerbatimHtmlRenderer.tsx +++ b/Clients/Xamarin.Interactive.Client.Web/ClientApp/renderers/VerbatimHtmlRenderer.tsx @@ -1,18 +1,12 @@ -// -// Author: -// Aaron Bockover -// // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. import * as React from 'react' import * as ReactDOM from 'react-dom' -import { randomReactKey } from '../utils' import { CodeCellResult } from '../evaluation' import { - ResultRenderer, - ResultRendererRepresentation, - getFirstRepresentationOfType + getFirstRepresentationOfType, + createComponentRepresentation } from '../rendering' import { ToStringRepresentationDataTypeName, @@ -21,37 +15,32 @@ import { const RepresentationTypeName = 'Xamarin.Interactive.Representations.VerbatimHtml' -export default function VerbatimHtmlRendererFactory(result: CodeCellResult) { - return getFirstRepresentationOfType(result, RepresentationTypeName) - ? new VerbatimHtmlRenderer - : null -} +export default function createVerbatimHtmlRepresentation(result: CodeCellResult) { + if (!getFirstRepresentationOfType(result, RepresentationTypeName)) + return null -class VerbatimHtmlRenderer implements ResultRenderer { - getRepresentations(result: CodeCellResult) { - // VerbatimHtml on the C# side intentionally does not expose the HTML data - // to serialization since we will always send a ToStringRepresentation. This - // avoids sending duplicate data across the wire. - // - // So, grab the ToStringRepresentation and render that as HTML instead. - const rep = getFirstRepresentationOfType( - result, - ToStringRepresentationDataTypeName) + // VerbatimHtml on the C# side intentionally does not expose the HTML data + // to serialization since we will always send a ToStringRepresentation. This + // avoids sending duplicate data across the wire. + // + // So, grab the ToStringRepresentation and render that as HTML instead. + const value = getFirstRepresentationOfType( + result, + ToStringRepresentationDataTypeName) - if (rep) - return [{ - key: randomReactKey(), - displayName: 'HTML', - component: VerbatimHtmlRepresentation, - componentProps: { - value: rep.formats[0].value - } - }] - return [] - } + if (!value) + return null + + return createComponentRepresentation( + 'HTML', + VerbatimHtmlRenderer, + { + value: value.formats[0].value + } + ) } -class VerbatimHtmlRepresentation extends React.Component< +class VerbatimHtmlRenderer extends React.Component< { value: string }, { width: number, height: number }> { constructor(props: any) { diff --git a/Clients/Xamarin.Interactive.Client.Web/ClientApp/rendering.ts b/Clients/Xamarin.Interactive.Client.Web/ClientApp/rendering.ts index da3ada1d5..0a868029d 100644 --- a/Clients/Xamarin.Interactive.Client.Web/ClientApp/rendering.ts +++ b/Clients/Xamarin.Interactive.Client.Web/ClientApp/rendering.ts @@ -6,8 +6,9 @@ // Licensed under the MIT License. import * as React from 'react' - +import { List, Record } from 'immutable' import { CodeCellResult } from './evaluation' +import { randomReactKey } from './utils' export const enum ResultRendererRepresentationOptions { None = 0, @@ -38,19 +39,39 @@ export const enum ResultRendererRepresentationOptions { SuppressDisplayNameHint = 8 } -export interface ResultRenderer { - getRepresentations(result: CodeCellResult): ResultRendererRepresentation[] +export interface Representation { + readonly key: React.Key + readonly displayName: string + readonly order?: number + readonly component?: any + readonly componentProps?: {} + readonly children?: List } -export type ResultRendererFactory = (result: CodeCellResult) => ResultRenderer | null +export function createComponentRepresentation( + displayName: string, + component: any, + componentProps?: {}, + order?: number) { + return { + key: randomReactKey(), + displayName, + component, + componentProps, + order + } +} -export interface ResultRendererRepresentation { - key: string - displayName: string - component: any - componentProps?: {} - order?: number - options?: ResultRendererRepresentationOptions +export function createContainerRepresentation( + displayName: string, + children: Representation[], + order?: number) { + return { + key: randomReactKey(), + displayName, + children: children && List(children), + order + } } export function getRepresentationsOfType(result: CodeCellResult, typeName: string): T[] { @@ -69,4 +90,62 @@ export function getFirstRepresentationOfType(result: CodeCellResult, typ return reps && reps.length > 0 ? reps[0] : null +} + +export type RepresentationFactory = (result: CodeCellResult) => Representation | null + +export class RepresentationRegistry { + private factories: List = List() + + constructor(...factories: RepresentationFactory[]) { + this.register(...factories) + } + + register(...factories: RepresentationFactory[]) { + this.factories = this.factories.push(...factories) + } + + getRepresentations(result: CodeCellResult): Representation | null { + let representations = List() + + if (result.isNullResult) + representations = representations.push(createNullRepresentation()) + else + this.factories.forEach(factory => { + const representation = factory && factory(result) + if (representation && (representation.component || + (representation.children && representation.children.size > 0))) + representations = representations.push(representation) + }) + + if (representations.size == 0) + return null + + return { + key: randomReactKey(), + displayName: '_root', + children: representations + } + } +} + +import createNullRepresentation from './renderers/NullRenderer' +import createToStringRepresentation from './renderers/ToStringRenderer' +import createImageRepresentation from './renderers/ImageRenderer' +import createVerbatimHtmlRepresentation from './renderers/VerbatimHtmlRenderer' +import createCalenderRepresentation from './renderers/CalendarRenderer' +import createTypeRepresentation from './renderers/TypeSystemRenderers' + +export function createDefaultRegistry() { + return new RepresentationRegistry( + // More exciting and specific renderers should be first + createCalenderRepresentation, + createImageRepresentation, + createVerbatimHtmlRepresentation, + createTypeRepresentation, + createToStringRepresentation) +} + +export function createDesignRegistry() { + return createDefaultRegistry() } \ No newline at end of file diff --git a/Clients/Xamarin.Interactive.Client.Web/ClientApp/utils.ts b/Clients/Xamarin.Interactive.Client.Web/ClientApp/utils.ts index 59aa09e07..29322603c 100644 --- a/Clients/Xamarin.Interactive.Client.Web/ClientApp/utils.ts +++ b/Clients/Xamarin.Interactive.Client.Web/ClientApp/utils.ts @@ -4,6 +4,60 @@ export function randomReactKey() { return uuidv4() } +interface ClassNamePredicates { + [className: string]: boolean | (() => boolean) +} + +/** + * Flatten an array of class names into a well formed string + * suitable for React's `className` property. + * + * @param names an array of CSS class names. Array elements may be string + * or an object whose property names map to a boolean or a predicate + * function to indicate whether the class name should be included. + * + * @example + * classNames( + * 'AlwaysInclude', + * { + * 'SometimesInclude: false, + * 'AlwaysInclude': true, + * 'MaybeInclude': () => Math.random() >= 0.5 + * }, + * null, + * undefined) => ['AlwaysInclude', 'MaybeInclude'?] + */ +export function classNames(...names: (ClassNamePredicates | string | undefined | null)[]): string { + const selectedNames: string[] = [] + + function selectName(name: string) { + if (selectedNames.indexOf(name) < 0) + selectedNames.push(name) + } + + for (const name of names) { + if (!name) + continue + + if (typeof name === 'string') { + selectName(name) + continue + } + + for (const key in name) { + if (name.hasOwnProperty(key)) { + let predicate = name[key] + if (predicate instanceof Function) + predicate = predicate() + if (predicate) + selectName(key) + } + } + } + + return selectedNames.join(' ') +} + let _osMac: boolean | undefined export function osMac() { if (_osMac === undefined) @@ -17,4 +71,14 @@ export function isSafari() { if (_isSafari === undefined) _isSafari = /Apple/i.test(navigator.vendor) return _isSafari +} + +export function debounce(action: () => void, delay: number = 0): () => void { + var debounceTimeout: number + return () => { + clearTimeout(debounceTimeout) + debounceTimeout = window.setTimeout( + () => window.requestAnimationFrame(action), + delay) + } } \ No newline at end of file diff --git a/Clients/Xamarin.Interactive.Client.Web/Icons.sketch b/Clients/Xamarin.Interactive.Client.Web/Icons.sketch new file mode 100644 index 0000000000000000000000000000000000000000..473d6c1778cd94d041303a0a90bf51bfcb1bd862 GIT binary patch literal 8375 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 NC-bpgU6Q|g{{czIl1Tsn literal 0 HcmV?d00001 From 18808bfc8f0ec280486814a7a97608a214a12987 Mon Sep 17 00:00:00 2001 From: Aaron Bockover Date: Sun, 3 Jun 2018 10:48:41 -0400 Subject: [PATCH 3/7] Web: style improvements for content --- .../ClientApp/components/Menu.tsx | 12 +++-- .../ClientApp/components/WorkbookEditor.scss | 44 ++++++++++++++++--- 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/Menu.tsx b/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/Menu.tsx index 86e3da616..cfbffc6a0 100644 --- a/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/Menu.tsx +++ b/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/Menu.tsx @@ -12,10 +12,16 @@ export const styleMap = { }, }; -export function getBlockStyle(block: Draft.ContentBlock): string { +export function getBlockStyle(block: Draft.ContentBlock): string | undefined { switch (block.getType()) { - case 'blockquote': return 'RichEditor-blockquote'; - default: return ""; + case 'blockquote': return 'Draft-Block-BlockQuote' + case 'header-one': return 'Draft-Block-H1' + case 'header-two': return 'Draft-Block-H2' + case 'header-three': return 'Draft-Block-H3' + case 'header-four': return 'Draft-Block-H4' + case 'header-five': return 'Draft-Block-H5' + case 'header-six': return 'Draft-Block-H6' + default: return 'Draft-Block-Default'; } } diff --git a/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/WorkbookEditor.scss b/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/WorkbookEditor.scss index 62f95a818..daa80c5e8 100644 --- a/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/WorkbookEditor.scss +++ b/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/WorkbookEditor.scss @@ -5,22 +5,52 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +@import '../css/theme.scss'; + +@import url('https://fonts.googleapis.com/css?family=Roboto+Condensed'); + .WorkbookEditor-container { display: flex; flex-flow: column; flex: 1 1 auto; overflow: auto; - padding: 1em 3em; + padding: 3em calc(15vw + 3em); cursor: text; - h1:first-child, - h2:first-child, - h3:first-child, - h4:first-child, - h5:first-child, - h6:first-child { + .Draft-Block-H1:first-child, + .Draft-Block-H2:first-child, + .Draft-Block-H3:first-child, + .Draft-Block-H4:first-child, + .Draft-Block-H5:first-child, + .Draft-Block-H6:first-child { margin-top: 0; } + + .Draft-Block-H1, + .Draft-Block-H2, + .Draft-Block-H3, + .Draft-Block-H4, + .Draft-Block-H5, + .Draft-Block-H6 { + font-family: "Roboto Condensed", sans-serif; + margin-top: 1.5em; + } + + .Draft-Block-Default, + .Draft-Block-BlockQuote { + font: 17px/1.5 "Iowan Old Style", "Apple Garamond", Baskerville, "Palatino Linotype", "Times New Roman", "Droid Serif", Times, "Source Serif Pro", serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + margin-bottom: 1em; + } + + .Draft-Block-BlockQuote { + padding: 1em; + margin: 0; + background: #deecf9; + } +} + +code { + @include codeFont; } \ No newline at end of file From c64c1adaebe3e4c700067439feb412e78c8e1117 Mon Sep 17 00:00:00 2001 From: Aaron Bockover Date: Sun, 3 Jun 2018 10:48:59 -0400 Subject: [PATCH 4/7] Web: Monaco feature parity with desktop --- .../ClientApp/components/MonacoCellEditor.tsx | 115 ++++++++++++++---- 1 file changed, 88 insertions(+), 27 deletions(-) diff --git a/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/MonacoCellEditor.tsx b/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/MonacoCellEditor.tsx index 686a2aff1..668c38a88 100644 --- a/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/MonacoCellEditor.tsx +++ b/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/MonacoCellEditor.tsx @@ -9,11 +9,12 @@ import * as React from 'react' import * as ReactDOM from 'react-dom' -import { osMac } from '../utils' +import { osMac, debounce } from '../utils' import { SelectionState } from 'draft-js' import { EditorMessage, EditorMessageType, EditorKeys } from '../utils/EditorMessages' import { CodeCellUpdate, DiagnosticSeverity } from '../evaluation' +const theme = require('../css/theme.scss') import './MonacoCellEditor.scss' export interface MonacoCellEditorProps { @@ -44,11 +45,35 @@ enum ViewEventType { } export class MonacoCellEditor extends React.Component { - private windowResizeHandler: any; - private editor?: monaco.editor.ICodeEditor; + private windowResizeHandler: any private lastUpdateResponse: CodeCellUpdate | null = null private markedTextIds: string[] = [] + private editor?: monaco.editor.ICodeEditor + + private editorOptions: monaco.editor.IEditorConstructionOptions = { + language: "csharp", + scrollBeyondLastLine: false, + roundedSelection: false, + overviewRulerLanes: 0, // 0 = hide overview ruler + formatOnType: true, + contextmenu: false, + cursorBlinking: 'phase', + renderIndentGuides: false, + minimap: { + enabled: false + }, + scrollbar: { + // must explicitly hide scrollbars so they don't interfere with mouse events + horizontal: 'hidden', + vertical: 'hidden', + handleMouseWheel: false, + useShadows: false, + } + } + + private debouncedUpdateLayout = debounce(this.updateLayout.bind(this), 250) + constructor(props: MonacoCellEditorProps) { super(props) this.state = { created: true } @@ -89,30 +114,14 @@ export class MonacoCellEditor extends React.Component { this.syncContent() @@ -311,7 +320,7 @@ export class MonacoCellEditor extends React.Component Date: Sun, 3 Jun 2018 10:49:33 -0400 Subject: [PATCH 5/7] Build.proj: invoke default targets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Do not explicitly invoke a “Build” target. --- build.proj | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/build.proj b/build.proj index b4e52606e..be07baf8b 100644 --- a/build.proj +++ b/build.proj @@ -181,8 +181,7 @@ + Projects="$(SolutionFile)"/> Date: Mon, 4 Jun 2018 10:31:51 -0700 Subject: [PATCH 6/7] Web: Fix missing status bar. --- .../ClientApp/components/StatusMessageBar.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/StatusMessageBar.tsx b/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/StatusMessageBar.tsx index 50440c36b..ca0068fe6 100644 --- a/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/StatusMessageBar.tsx +++ b/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/StatusMessageBar.tsx @@ -112,13 +112,15 @@ export class StatusMessageBar extends React.Component 0 + ? rightCommands[rightCommands.length - 1].getBoundingClientRect().left + : sideCommands.getBoundingClientRect().right this.setState({ bounds: { top: leftBounds.top + 'px', left: leftBounds.right + 'px', - width: (rightBounds.left - leftBounds.right) + 'px', + width: (rightBounds - leftBounds.right) + 'px', height: leftBounds.height + 'px' } }) From 3b4d47440a401a7a38a494c8eafd749520b31593 Mon Sep 17 00:00:00 2001 From: Aaron Bockover Date: Thu, 7 Jun 2018 15:25:32 -0700 Subject: [PATCH 7/7] Squash: Web: more UI tweaks --- .../ClientApp/components/CodeCellView.scss | 12 +++++++++-- .../ClientApp/components/CodeCellView.tsx | 21 ++++++++++++++----- .../ClientApp/components/Dropdown.scss | 12 +++++++++++ .../components/RepresentationSelector.scss | 1 + .../components/RepresentationSelector.tsx | 15 ++++++------- .../ClientApp/renderers/ImageRenderer.scss | 9 ++++---- .../ClientApp/renderers/ImageRenderer.tsx | 3 +-- .../ClientApp/renderers/ToStringRenderer.tsx | 8 +++---- 8 files changed, 55 insertions(+), 26 deletions(-) diff --git a/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/CodeCellView.scss b/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/CodeCellView.scss index 7017cfff1..699547a7e 100644 --- a/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/CodeCellView.scss +++ b/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/CodeCellView.scss @@ -98,18 +98,26 @@ $borderColor: darken($cellEditorBackground, 20%); .CodeCell-results-container, .CodeCell-captured-output-container { margin-bottom: $margin; + padding: 0; .CodeCell-representation { display: flex; .RepresentationSelector { - margin-left: calc(-10vw - 1em); + flex-shrink: 0; + margin-left: calc(-10vw - #{$padding}); + padding: $padding $padding 0 0; width: 10vw; + + .DropdownButton { + padding-top: calc((1.5em - 1.0em) / 2); + } } .CodeCell-representation-renderer-container { flex-grow: 1; - padding-left: calc(1em + #{$padding}); + padding: $padding 0 0 $padding; + margin: 0; } } } diff --git a/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/CodeCellView.tsx b/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/CodeCellView.tsx index ffe1bf696..728928031 100644 --- a/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/CodeCellView.tsx +++ b/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/CodeCellView.tsx @@ -41,12 +41,22 @@ interface RepresentationViewState { selectedRepresentation?: Representation } -class RepresentationView extends React.PureComponent { +class RepresentationView extends React.Component { constructor(props: RepresentationViewProps) { super(props) this.state = {} } + shouldComponentUpdate(nextProps: RepresentationViewProps, nextState: RepresentationViewState) { + function getKey(representation: Representation | undefined) { + if (representation) + return representation.key + return undefined + } + + return getKey(this.state.selectedRepresentation) !== getKey(nextState.selectedRepresentation) + } + render() { return (
@@ -54,12 +64,13 @@ class RepresentationView extends React.PureComponent this.setState({ selectedRepresentation })}/> -
- {this.state.selectedRepresentation && this.state.selectedRepresentation.component && + {this.state.selectedRepresentation && this.state.selectedRepresentation.component && +
} -
+ {... this.state.selectedRepresentation.componentProps}/> +
+ }
) } diff --git a/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/Dropdown.scss b/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/Dropdown.scss index ecf47818e..9a5a00165 100644 --- a/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/Dropdown.scss +++ b/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/Dropdown.scss @@ -16,8 +16,20 @@ button { border: none; + margin: 0; + padding: 0; + width: auto; + overflow: visible; text-align: inherit; + background: inherit; + color: inherit; + font: inherit; + line-height: normal; + -webkit-font-smoothing: inherit; + -moz-osx-font-smoothing: inherit; + -webkit-appearance: none; + .DropdownButtonExpander { svg { stroke: currentColor; diff --git a/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/RepresentationSelector.scss b/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/RepresentationSelector.scss index 3761a0af1..f638e8495 100644 --- a/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/RepresentationSelector.scss +++ b/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/RepresentationSelector.scss @@ -19,6 +19,7 @@ .DropdownButton { border-bottom: 1px solid transparent; + background: transparent; } &.DropdownActive .DropdownButton, diff --git a/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/RepresentationSelector.tsx b/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/RepresentationSelector.tsx index 811083598..5bce09314 100644 --- a/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/RepresentationSelector.tsx +++ b/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/RepresentationSelector.tsx @@ -109,13 +109,14 @@ export class RepresentationSelector extends React.PureComponent< const inSelectionPath = selectedPath[0].key === representation.key - dropdowns.push( transitionHeight(elem, inSelectionPath)} - key={dropdowns.length} - options={options} - defaultSelectedKey={inSelectionPath && selectedPath.length > 1 ? selectedPath[1].key : undefined} - onChanged={this.onSelectedKeyChanged}/>) + if (options.length > 1) + dropdowns.push( transitionHeight(elem, inSelectionPath)} + key={dropdowns.length} + options={options} + defaultSelectedKey={inSelectionPath && selectedPath.length > 1 ? selectedPath[1].key : undefined} + onChanged={this.onSelectedKeyChanged}/>) representation.children.forEach(child => { this.renderDropdowns( diff --git a/Clients/Xamarin.Interactive.Client.Web/ClientApp/renderers/ImageRenderer.scss b/Clients/Xamarin.Interactive.Client.Web/ClientApp/renderers/ImageRenderer.scss index 393ddbdb8..48aceb282 100644 --- a/Clients/Xamarin.Interactive.Client.Web/ClientApp/renderers/ImageRenderer.scss +++ b/Clients/Xamarin.Interactive.Client.Web/ClientApp/renderers/ImageRenderer.scss @@ -1,14 +1,13 @@ -// -// Author: -// Aaron Bockover -// // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -.renderer-ImageRepresentation-container { +.CodeCell-representation-ImageRepresentation { > img { max-width: 100vw; height: auto; width: auto; } + + padding: 0 !important; + line-height: 0 !important; } \ No newline at end of file diff --git a/Clients/Xamarin.Interactive.Client.Web/ClientApp/renderers/ImageRenderer.tsx b/Clients/Xamarin.Interactive.Client.Web/ClientApp/renderers/ImageRenderer.tsx index 943d61132..2b9b5798a 100644 --- a/Clients/Xamarin.Interactive.Client.Web/ClientApp/renderers/ImageRenderer.tsx +++ b/Clients/Xamarin.Interactive.Client.Web/ClientApp/renderers/ImageRenderer.tsx @@ -39,10 +39,9 @@ export default function createImageRepresentation(result: CodeCellResult) { class ImageRepresentation extends React.Component { render() { const image = this.props; - const size = image.width > 0 ? { width: image.width, height: image.height} : {} const src = image.format === 'Uri' ? atob(image.data.$value) : `data:${image.format};base64,${image.data.$value}` - return + return } } \ No newline at end of file diff --git a/Clients/Xamarin.Interactive.Client.Web/ClientApp/renderers/ToStringRenderer.tsx b/Clients/Xamarin.Interactive.Client.Web/ClientApp/renderers/ToStringRenderer.tsx index ef52ccb8e..035ec59ff 100644 --- a/Clients/Xamarin.Interactive.Client.Web/ClientApp/renderers/ToStringRenderer.tsx +++ b/Clients/Xamarin.Interactive.Client.Web/ClientApp/renderers/ToStringRenderer.tsx @@ -10,6 +10,8 @@ import { createComponentRepresentation } from '../rendering' +import './ToStringRenderer.scss' + export const ToStringRepresentationDataTypeName = 'Xamarin.Interactive.Representations.ToStringRepresentation' export interface ToStringRepresentationData { @@ -28,16 +30,12 @@ export default function createToStringRepresentation(result: CodeCellResult): Re if (!value) return null - const rr = createContainerRepresentation( + return createContainerRepresentation( 'ToString', value.formats.map(format => createComponentRepresentation( format.name, ToStringRenderer, { value: format.value }))) - - console.log("CREATED REP: %O", rr) - - return rr } class ToStringRenderer extends React.Component<{ value: string }> {